Make Your Own Sugar
Activities!
Copyright : The Contributors (see back)
Published : 2010-10-08
License : CC-BY-SA
Note : We offer no warranty if you follow this manual and something goes wrong.
So be careful!
Table of Contents
SUGAR ACTIVITIES
1 Introduction 2
2 What is Sugar? 5
3 What is a Sugar Activity? 9
4 What Do I Need To Know To Write A Sugar Activity? 10
PROGRAMMING
5 Setting Up a Sugar Development Environment 14
6 Creating your First Sugar Activity 23
7 A Standalone Python Program For Reading Etexts 25
8 Inherit From sugar. activityActivity 32
9 Package The Activity 39
10 Add Refinements 47
11 Add Your Activity Code To Version Control 61
12 Going International With Pootle 80
13 Distribute Your Activity 86
14 Debugging Sugar Activities 91
ADVANCED TOPICS
15 Making Shared Activities 100
16 Adding Text To Speech 153
1 7 Fun With The Journal 1 72
18 Making Activities Using PyGame 197
19 Making New Style Toolbars 210
APPENDIX
20 Where To Go From Here? 234
21 About The Authors 236
22 License 238
SUGAR ACTIVITIES
1. Introduction
2. What is Sugar?
3. What is a Sugar Activity?
4. What Do I Need To Know To Write A Sugar Activity?
A • Introduction
"This book is a record of a pleasure trip. If it were a record of a solemn scientific
expedition, it would have about it that gravity, that profundity, and that impressive
incomprehensibility which are so proper to works of that kind, and withal so attractive."
From the Preface to The Innocents Abroad, by Mark Twain
The purpose of this book is to teach you what you need to know to write Activities for
Sugar, the operating environment developed for the One Laptop Per Child project. This
book does not assume that you know how to program a computer, although those who
do will find useful information in it. My primary goal in writing it is to encourage non
programmers, including children and their teachers, to create their own Sugar
Activities. Because of this goal I will include some details that other books would leave
out and leave out things that others would include. Impressive incomprehensibility will
be kept to a minimum.
If you just want to learn how to write computer programs Sugar provides many
Activities to help you do that: Etoys, Turtle Art, Scratch, and Pippy None of these are
really suitable for creating Activities so I won't cover them in this book, but they're a
great way to leam about programming. If you decide after playing with these that you'd
like to try writing an Activity after all you'll have a good foundation of knowledge to
build on.
When you have done some programming then you'll know how satisfying it can be to
use a program that you made yourself, one that does exactly what you want it to do.
Creating a Sugar Activity takes that enjoyment to the next level. A useful Sugar
Activity can be translated by volunteers into every language, be downloaded hundreds
of times a week and used every day by students all over the world.
/ • ■ * *
A book that teaches everything you need to know to write Activities would be really,
really long and would duplicate material that is already available elsewhere. B ecause of
this, I am going to write this as sort of a guided tour of Activity development. That
means, for example, that I'll teach you what Python is and why it's important to learn it
but I won't teach you the Python language itself. There are excellent tutorials on the
Internet that will do that, and I'll refer you to those tutorials.
There is much sample code in this book, but there is no need for you to type it in to try
it out. All of the code is in a Git repository that you can download to your own
computer. If you've never used Git there is a chapter that explains what it is and how to
use it.
I started writing Activities shortly after I received my XO laptop. When I started I didn't
know any of the material that will be in this book. I had a hard time knowing where to
begin. What I did have going for me though was a little less than 30 years as a
professional programmer. As a result of that I think like a programmer. A good
programmer can take a complex task and divide it up into manageable pieces. He can
figure out how things must work, and from that figure out how they do work. He knows
how to ask for help and where. If there is no obvious place to begin he can begin
somewhere and eventually get where he needs to go.
Because I went through this process I think I can be a pretty good guide to writing
Sugar Activities. Along the way I hope to also teach you how to think like a
programmer does.
From time to time I may add chapters to this book. Sugar is a great application platform
and this book can only begin to tell you what is possible. It is my hope that future
versions of the book will have guest chapters on more advanced topics written by other
experienced Activity developers.
Za • What is Sugar?
Sugar is the user interface designed for the XO laptop. It can now be installed on most
PCs, including older models that can't run the latest Windows software. You can also
install it on a thumb drive (Sugar on a Stick) and boot your PC from that.
When the XO laptop first came out some people questioned the need for a new user
interface. Wouldn't it be better for children to learn something more like what they
would use as adults? Why not give them Microsoft Windows instead?
This would be a reasonable question if the goal was to train children to use computers
and nothing else. It would be even more reasonable if we could be sure that the
software they would use as adults looked and worked like the Microsoft Windows of
today. These are of course not reasonable assumptions.
The OLPC project is not just about teaching computer literacy. It is about teaching
everything: reading, writing, arithmetic, history, science, arts and crafts, computer
programming, music composition, and everything else. Not only do we expect the child
to use the computer for her school work, we expect her to take it home and use it for her
own explorations into subjects that interest her.
This is a great deal more than anyone has done with computers for education, so it is
reasonable to rethink how children should work with computers. Sugar is the result of
that rethinking.
Sugar has the following unique features:
The Journal
The Journal is where all the student's work goes. Instead of files and folders there is a
list of Journal entries. The list is sorted in descending order by the date and time it was
last worked on. In a way it's like the "Most Recently Used" document menu in
Windows, except instead of containing just the last few items it contains everything and
is the normal way to save and resume work on something.
The Journal makes it easy to organize your work. Any work you do is saved to the
Journal. Anything you download from the web goes in the Journal. If you've ever
downloaded a file using a web browser, then had to look for it afterwards because it
went in some directory other than the one you expected, or if you ever had to help your
parents when they were in a similar situation, you can understand the value of the
Journal.
The Journal has metadata for each item in it. Metadata is information about information.
Every Journal entry has a title, a description, a list of keywords, and a screen shot of
what it looked like the last time it was used. It has an activity id that links it to the
Activity that created it, and it may have a MIME type as well (which is a way of
identifying Journal entries so that items not created by an Activity may still be used by
an Activity that supports that MIME type).
In addition to these common metadata items a Journal entry may be given custom
metadata by an Activity. For instance, the Read Activity uses custom metadata to save
the page number you were reading when you quit the Activity. When you resume
reading later the Activity will put you on that page again.
In addition to work created by Activities, the Journal can contain Activities themselves.
To install an Activity you can use the Browse Activity to visit the website
http : //activities . sugarlab s . org and download it. It will automatically be saved to the
Journal and be ready for use. If you don't want the Activity any more, simply delete it
from the Journal and it's completely gone. No uninstall programs, no dialog boxes telling
you that such and such a .DLL doesn't seem to be needed anymore and do you want to
delete it? No odd bits and pieces left behind.
Collaboration
The second unique feature Sugar is Collaboration. Collaboration means that Activities
can be used by more than one person at the same time. While not every Activity needs
collaboration and not every Activity that could use it supports it, a really first rate
Activity will provide some way to interact with other Sugar users on the network. For
instance, all the e-book reading Activities provide a way of giving a copy of the book
you're reading (with any notes you added to it) to a friend or to the whole class. The
Write Activity lets several students work on the same document together. The
Distance Activity lets two students see how far apart from each other they are.
There are five views of the system you can switch to at the push of a button (Function
Keys Fl-4). They are:
• The Neighborhood View
• The Friends View
• The Activity Ring
• The Journal
Of these Views, the first two are used for Collaboration.
The Neighborhood View shows icons for everyone on the network. Every icon looks
like a stick figure made by putting an "O" above an "X". Each icon has a name, chosen
by the student when she sets up her computer. Every icon is displayed in two colors,
also chosen by the student. In addition to these "XO" icons there will be icons
representing mesh networks and others representing WiFi hot spots. Finally there will
be icons representing active Activities that their owners wish to share.
To understand how this works consider the Chat Activity. The usual way applications
do chat is to have all the participants start up a chat client and visit a particular chat
room at the same time. With Sugar it's different. One student starts the Chat Activity on
her own computer and goes to the Neighborhood View to invite others on the network
to participate. They will see a Chat icon in their own Neighborhood View and they can
accept. The act of accepting starts up their own Chat Activity and connects them to the
other participants.
The Friends View is similar to the Neighborhood View, but only contains icons for
people you have designated as Friends. Collaboration can be offered at three levels: with
individual persons, with the whole Neighborhood, and with Friends. Note that the
student alone decides who her Friends are. There is no need to ask to be someone's
Friend. It's more like creating a mailing list in email.
Security
Protecting computers from malicious users is very important, and if the computers
belong to students it is doubly important. It is also more difficult, because we can't
expect young students to remember passwords and keep them secret. Since Sugar runs
on top of Linux viruses aren't much of a problem, but malicious Activities definitely are.
If an Activity was allowed unrestricted access to the Journal, for instance, it could wipe it
out completely. Somebody could write an Activity that seems to be harmless and
amusing, but perhaps after some random number of uses it could wipe out a student's
work.
The most common way to prevent a program from doing malicious things is to make it
run in a sandbox. A sandbox is a way to limit what a program is allowed to do. With the
usual kind of sandbox you either have an untrusted program that can't do much of
anything or a trusted program that is not restricted at all. An application becomes
trusted when a third party vouches for it by giving it a signature. The signature is a
mathematical operation done on the program that only remains valid if the program is
not modified.
Sugar has a more sophisticated sandbox for Activities than that. No Activity needs to be
trusted or is trusted. Every Activity can only work with the Journal in a limited, indirect
way. Each Activity has directories specific to it that it can write to, and all other
directories and files are limited to read-only access. In this way no Activity can interfere
with the workings of any other Activity. In spite of this, an Activity can be made to do
what it needs to do.
Summary
Sugar is an operating environment designed to support the education of children. It
organizes a child's work without needing files and folders. It supports collaboration
between students. Finally, it provides a robust security model that prevents malicious
programs from harming a student's work.
It would not be surprising to see these features someday adopted by other desktop
environments.
*5 • What is a Sugar Activity?
A Sugar Activity is a self-contained Sugar application packaged in a .xo bundle.
An .xo bundle is an archive file in the Zip format. It contains:
• A MANIFEST file listing everything in the bundle
• An activity.info file that has attributes describing the Activity as name=value
pairs. These attributes include the Activity name, its version number, an identifier,
and other things we will discuss when we write your first Activity.
• An icon file (in SVG format)
• Files containing translations of the text strings the Activity uses into many
languages
• The program code to run the Activity
A Sugar Activity will generally have some Python code that extends a Python class
called Activity. It may also make use of code written in other languages if that code is
written in a way that allows it to be used from Python (this is called having Python
bindings). It is even possible to write a Sugar Activity without using Python at all, but
this is beyond the scope of this book.
There are only a few things that an Activity can depend on being included with every
version of Sugar. These include modules like Evince (PDF and other document
viewing), Gecko (rendering web pages), and Python libraries like PyGTK and PyGame.
Everything needed to run the Activity that is not supplied by Sugar must go in the
bundle file. A question sometimes heard on the mailing lists is "How do I make Sugar
install X the first time my Activity is run?" The answer: you don't. If you need X it
needs to go in the bundle.
You can install an Activity by copying or downloading it to the Journal. You uninstall it
by removing it from the Journal. There is no Install Shield to deal with, no deciding
where you want the files installed, no possibility that installing a new Activity will make
an already installed Activity stop working.
An Activity generally creates and reads objects in the Journal. A first rate Activity will
provide some way for the Activity to be shared by multiple users.
4 • What Do I Need To Know To Write A
Sugar Activity?
If you are going to write Sugar Activities you should learn something about the topics
described in this chapter. There is no need to become an expert in any of them, but you
should bookmark their websites and skim through their tutorials. This will help you to
understand the code samples we'll be looking at.
Python
Python is the most used language for writing Activities. While you can use other
languages, most Activities have at least some Python in them. Sugar provides a Python
API that simplifies creating Activities. While it is possible to write Activities using no
Python at all (like Etoys), it is unusual.
All of the examples in this book are written entirely in Python.
There are compiled languages and interpreted languages. In a compiled language the
code you write is translated into the language of the chip it will run on and it is this
translation that is actually run by the OS. In an interpreted language there is a program
called an interpreter that reads the code you write and does what the code tells it to do.
(This is over simplified, but close enough to the truth for this chapter).
Python is an interpreted language. There are advantages to having a language that is
compiled and there are advantages to having an interpreted language. The advantages
Python has for developing Activities are:
10
• It is portable. In other words, you can make your program run on any chip and
any OS without making a version specific to each one. Compiled programs only
run on the OS and chip they are compiled for.
• Since the source code is the thing being run, you can't give someone a Python
program without giving them the source code. You can learn a lot about Activity
programming by studying other people's code, and there is plenty of it to study.
• It is an easy language for new programmers to learn, but has language features that
experienced programmers need.
• It is widely used. One of the best known Python users is Google. They use it
enough that they have started a project named "Unladen Swallow" to make
Python programs run faster.
The big advantage of a compiled language is that it can run much faster than an
interpreted language. However, in actual practice a Python program can perform as well
as a compiled program. To understand why this is you need to understand how a
Python program is made.
Python is known as a "glue" language. The idea is that you have components written in
various languages (usually C and C++) and they have Python bindings. Python is used
to "glue" these components together to create applications. In most applications the
bulk of the application's function is done by these compiled components, and the
application spends relatively little time running the Python code that glues the
components together.
In addition to Activities using Python most of the Sugar environment itself is written in
Python.
If you have programmed in other languages before there is a good tutorial for learning
Python at the Python website: http : //docs. python. org/tutorial/ . If you're just starting
out in programming you might check out Invent Your Own Computer Games With
Python, which you can read for free at http://inventwithpython.com/ .
PyGTK
GTK+ is a set of components for creating user interfaces. These components include
things like buttons, scroll bars, list boxes, and so on. It is used by GNOME desktop
environment and the applications that run under it. Sugar Activities use a special
GNOME theme that give GTK+ controls a unique look.
11
PyGTK is a set of Python bindings that let you use GTK+ components in Python
programs. There is a tutorial showing how to use it at the PyGTK website:
http : //www.py gtk. org/tutorial. html .
PyGame
The alternative to using PyGTK for your Activity is PyGame. PyGame can create
images called sprites and move them around on the screen. As you might expect,
PyGame is mostly used for writing games. It is less commonly used in Activities than
PyGTK.
The tutorial to learn about PyGame is at the PyGame website:
http://www.pygame.org/wiki/tutorials . The website also has a bunch of pygame projects
you can download and try out.
12
PROGRAMMING
5. Setting Up a Sugar Development Environment
6. Creating your First Sugar Activity
7. A Standalone Python Program For Reading Etexts
8. Inherit From sugar.activity.Activity
9. Package The Activity
10. Add Refinements
11. Add Your Activity Code To Version Control
12. Going International With Pootle
13. Distribute Your Activity
14. Debugging Sugar Activities
13
3 • Setting Up a Sugar Development
Environment
It is not currently practical to develop Activities for the XO on the XO. It's not so much
that you can't do it, but that it's easier and more productive to do your development
and testing on another machine running a more conventional OS. This gives you access
to better tools and it also enables you to simulate collaboration between two computers
running Sugar using only one computer.
Install Linux Or Use A Virtual Machine?
Even though Sugar runs on Linux it is possible to run a complete instance of Sugar in a
virtual machine that runs on Windows. A virtual machine is a way to run one operating
system on top of another one. The operating system being run is fooled into thinking it
has the whole computer to itself. (Computer industry pundits will tell you that using
virtual machines is the newest new thing out there. Old timers like me know that IBM
was doing it on their mainframe computers back in the 1970's).
For awhile this was actually the recommended way to develop Activities. The version of
Linux that Sugar used was different enough from regular Linux distributions that even
Linux users were running Sugar in a virtual machine on top of Linux.
The situation has improved, and most current Linux distributions have a usable Sugar
environment.
If you're used to Windows you might think that running Sugar in a VM from Windows
instead of installing Linux might be the easier option. In practice it is not. Linux running
in a VM is still Linux, so you're still going to have to learn some things about Linux to
do Activity development. Also, running a second OS in a VM requires a really powerful
machine with gigabytes of memory. On the other hand, I do my Sugar development
using Linux on an IBM NetVista Pentium IV I bought used for a little over a hundred
dollars, shipping included. It is more than adequate.
Installing Linux is not the test of manhood it once was. Anyone can do it. The GNOME
desktop provided with Linux is very much like Windows so you'll feel right at home
using it.
14
When you install Linux you have the option to do a dual boot, running Linux and
Windows on the same computer (but not at the same time). This means you set aside a
disk partition for use by Linux and when you start the computer a menu appears asking
which OS you want to start up. The Linux install will even create the partition for you,
and a couple of gigabytes is more than enough disk space. Sharing a computer with a
Linux installation will not affect your Windows installation at all.
Sugar Labs has been working to get Sugar included with all Linux distributions. If you
already have a favorite distribution, chances are the latest version of it includes Sugar.
Fedora, openSuse, Debian, and Ubuntu all include Sugar. If you already use Linux, see
if Sugar is included in your distribution. If not, Fedora is what is used by the XO
computer so Fedora 10 or later might be your best bet. You can download the Fedora
install CD or DVD here: https://fedoraproject.org/get-fedora .
It is worth pointing out that all of the other tools I'm recommending are included in
every Linux distribution, and they can be installed with no more effort than checking a
check box. The same tools often will run on Windows, but installing them there is more
work than you would expect for Windows programs.
If you are unwilling to install and learn about Linux but still want to develop Activities
one option you have is to develop a standalone Python program that uses PyGame of
PyGTK and make it do what you'd like your Activity to do. You could then turn over
your program to someone else who could convert it into a Sugar Activity. You could
develop such a Python program on Windows or on the Macintosh.
If you want to do development on a Macintosh running Sugar in a virtual machine may
be a more attractive option. If you want to try it details will be found here:
http : //wiki. laptop. org/go/D ev elopers/Setup. It may also be possible to install Fedora
Linux on an Intel or Power PC Macintosh as a dual boot, just like you can do with
Windows. Check the Fedora website for details.
Another option for Mac users is to use Sugar on a Stick as a test environment. You can
learn about that here: http://wiki.sugarlabs.org/go/Sugar on a Stick .
15
What About Using sugar-jhbuild?
Sugar-jhbuild is a script that downloads the source code for the latest version of all the
Sugar modules and compiles it into a subdirectory of your home directory. It doesn't
actually install Sugar on your system. Instead, you run it out of the directory you
installed it in. Because of the way it is built and run it doesn't interfere with the
modules that make up your normal desktop. If you are developing Sugar itself, or if you
are developing Activities that depend on the very latest Sugar features you'll need to
run sugar-jhbuild.
Running this script is a bit more difficult than just installing the Sugar packages that
come with the distribution. You'll need to install Git and Subversion, run a Git
command from the terminal to download the sugar-jhbuild script, then run the script
with several different options which download more code, ask you to install more
packages, and ultimately compile everything. It may take you a couple of hours to do
all the steps. When you're done you'll have an up to date test environment that you can
run as an alternative to sugar-emulator. There is no need to uninstall sugar-emulator;
both can coexist.
You run it with these commands:
cd sugar-jhbuild
./sugar-jhbuild run sugar-emulator
Should you consider using it? The short answer is no. A longer answer is probably not
yet.
If you want your Activities to reach the widest possible audience you don 't want the
latest Sugar. In fact, if you want a test environment that mimics what is on most XO
computers right now you need to use Fedora 10. Because updating operating systems in
the field can be a major undertaking for a school most XO's will be running Sugar .82 or
older for quite some time.
Of course it is also important to have developers that want to push the boundaries of
what Sugar can do. If after developing some Activities you decide you need to be one of
them you can learn about running sugar-jhbuild here:
http://wiki.sugarlabs.org/go/DevelopmentTeam/Jhbuild.
Strictly speaking sugar-jhbuild is just the script that downloads and compiles Sugar. If
you wanted to be correct you would say "Run the copy of sugar-emulator you made
with sugar-jhbuild". Most Sugar developers would just say "Run sugar-jhbuild" and
that's what I'll say in this book.
16
Python
We'll be doing all the code samples in Python so you'll need to have Python installed.
Python comes with every Linux distribution. You can download installers for Windows
and the Macintosh at http : 1 1 www, python, org/ .
Eric
Developers today expect their languages to be supported by an Integrated
Development Environment and Python is no exception. An IDE helps to organize
your work and provides text editing and a built in set of programming and debugging
tools.
BookExamples - /h ome/li mj olpc/ bo okexamples/rn a In line/flea dE texts-Act
File Edit View Start Debug ynittcst
Mulbproiect Proiect Extras Settings Window Bookmarks Plugins Help
~| v » *\ » C » \- » *|Q
v » fe.
Vertical Toolbox
r Project-Viewer
m a
Read EtextsActJvtty py
Name
tj leitview
^ zf
fP _Jnit_(self. handle]
<p forit_de crease (self)
<p fontjn. crease (self)
(p keypresscMself, widi
(P makenew filenametss
page next! self)
(p page_previous(self)
tp read_file( self, filename
cp save_extracted_file(se
(p scroll_down(self)
'-,, Multiproject-Viewer
Template-viewer
Horizontal Toolbox
75
76
77
7B
96
37 -
L
self .2ho w_pa q c(paqel
^adjustment » £elf.scrulled window. get vadjustrnent()
wadjustrnent., value b ^adjustment, upp^r- ■u_adjustrriefit: l page_si
def psqejiextfsclff:
global pa
page-page+1
if page >m ien(sclf.page_iindrK): pnge"0
I". ■:'>: ...>:;■' i ■■■■■■ j
■^adjustment «= «lT.£crolled_i\iindow.get_vaa'jLi£trrierit(j
v_adjusbnenL value ■ v_adju5lm-ent.loweT
tier fontdeereaietseir):
font_slze - s.elT.fom_desc.get_slze(| f 1G24
fonE_size ■ font_size - 1
If font_size < 1:
! font_£|ze = 1
5rlf.Font_de3c.5e i t_5izc-(FiDnt_5ize * 1024)
&e I f.teatvievr. modify fDnt(5elf.font descj
der fnnil Jniif-rt'.f (ifir):
font_sizc • 5clf.font_dc5c.gct_5izeO / 1Q H 24
:
1 Python 2 5.2 <r2£2: 60911, Sep 30 2008-, 15:41:33)
2 [GCC 4.3.2 2DOH09L7 (fled Hat 4 5 2-4}] on olpcS.simnnons, standard
3 >5»*
There are two Python IDE's I have tried: Eric and Idle. Eric is the fancier of the two and
I recommend it. Every Linux distribution should include it. It looks like it might work
on Windows too. You can learn more about it at the Eric website: http : //eric-ide . py thon-
projects.org/ .
17
SPE (Stani's Python Editor)
This is an IDE I discovered while writing this book. It comes with Fedora and in
addition to being a Python editor it will make UML diagrams of your code and show
PyDoc for it. Here is SPE showing a UML diagram for one of the Activities in this
book:
tie en 2^ ]i(*i \j*t> tjc»
■ J J * ■<? *> '* A £
ifgifliHifia
l im Ii Hii-r^sm
sanaa
,n-
^ ■ 'i ■' ■ 1
^1 j _ . i . . . t i ' ■ j ■
*i "
tu^a
I... -. ' '..
tea
J
r
,T*_i-vMrhvin^i
'k"j/*V: MM* ailfcilr •.Hze'.tWjl
II Wk d.v>*f±JlDii!
]i.»™-d..™|^tWtnil
i igtjin <ii r |i irt I
JlfllJIWIBH
ro™i.™ri.rti(ipKlirt«ni
Mrf"hu™T1rc»J
iMh igfinMfr«mi«Mtfinwiiit
... ,■■■■» ,■ to ■M.-nrfv-
ttanlHUp *: fun --■■ - 1' ..■:" -.-■' t. : ■ -i j
■:..,ii i moq
kmiLi»ir.
thaw jrogM wl I pjig^.ruT* > r.i
JtL ?lwrtJ^»T ■'lUfK-iMH*!
• mti u.Lw iurtj*ilia<J
*-»l 1 "*! I art L rt*HW I
-II.. 'K-.Jt| | P**W
J
;
rVIilii iniii TUUflitittii
Iiuie tun bii^mUi I
, ■■■. rt.i'.m ji.+--. rm
««tm7H«vfB'i«nnMra«M4t«iii WMtf i nau a»y
rd.mn£«
If you're an experienced developer you might find this a useful alternative to Eric. If
you're just starting out Eric should meet your needs pretty well.
Other IDE's
There is also a commercial Python IDE called Wingware, which has a version you can
use for free. You can learn more about it at http://www.wingware.com/ .
18
Inkscape
Inkscape is a tool for creating images in SVG format. Sugar uses SVG for Activity icons
and other kinds of artwork. The "XO" icon that represents each child in the
Neighborhood view is an SVG file that can be modified.
r^ad-eteat&.svg - lrtk&cape
File Edit view Layer Object Path Tea. Erects Whiteboard Help
SI HI l3i £t £ ih "^ JJ iJ fa i"t a l&ow |;| r |q. dod |;| w |a,o(n |*'- ^ h fo.wi \r\ \t* |v| Affect | =5 | y
123 I 1J |]P l-J ."J 13 |W |LJ |Z[ G3 lif 133 ffl 1*1 IW |H HO B1 n
_i I i i ■ i < i i i i I ii n I i i i i 1 l i i i I i i i i I i ii i I i i i i I i i i m i i i I i i i i I i i i i I i i i i I i i i i I i i i i I i i i i I ii i i I i i i i "-^
re :
£ -
u ■-
or-
■& -
s
>
t& ^ -L*r«r i v NiiiabiMt3 4ei«s«taick r siis™+tiic*:.flrdr*g»raurt«j»aitflMH!a
Inkscape comes with every Linux distribution, and can be installed on Windows as
well. You can learn more about it here: http: //www. inkscape . org/ .
19
Git
Git is a version control system. It stores versions of your program code in a way that
makes them easy to get back. Whenever you make changes to your code you ask Git to
store your code in its repository. If you need to look at an old version of that code later
you can. Even better, if some problem shows up in your code you can compare your
latest code to an old, working version and see exactly what lines you changed.
readtrjolbar.py
427
427
42S
428
429
429
430
431
430
432
431
433
432
IB M along with this program; it not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 92119-1391
17
18 import os
19 import logging
20 from gettext import gettext as
21 import re
combotool.show()
self .pitchadj = gtk. Adjustment (B, -ISO, ISO, 1, IB, 0)
self .pitchadj . connect ("value_changed", self .pitch_adjusted_cb)
pitchbar = gtk. HScale(self .pitchadj)
pitchbar , set_draw_value(False)
pitchbar. set_update_policjf (gtk. UPDATE_DISCDNTINU0US)
pitchbar, show()
self.rateadj = gtk, Adjustment^, -IBS, IBB, 1, 13, B)
self. rateadj , connect ("value_changed", self, rate_adjusted_cb)
ratebar = gtk. HScale(self . rateadj )
ratebar , set_draw_value(False)
ratebar , set_update_policy (gtk. UPDATE_DISC0NnNU0US )
453 453 def pitch_adjusted_cb(self , get]:
454 454 speech, pitch = int (get, value)
455 455 speech, say(_("pitch adjusted"))
455 f = open (os, path, join (self .activity, get_activity_root(),
'insta
If there are two people working on the same program independently a version control
system will merge their changes together automatically.
Suppose you're working on a major new version of your Activity when someone finds a
really embarrassing bug in the version you just released. If you use Git you don't need
to tell people to live with it until the next release, which could be months away. Instead
you can create a branch of the previous version and work on it alongside the version
you're enhancing. In effect Git treats the old version you're fixing and the version you're
improving as two separate projects.
You can learn more about Git at the Git website: http: //git-scm. com/ .
20
When you're ready for a Git repository for your project you can set one up here:
http : //git . sugar lab s , or g/ . I will have more to say about setting up and using a Git
repository later in this book.
There is a Git repository containing all the code examples from this book. Once you
have Git installed you can copy the repository to your computer with this command:
git clone git : //git . sugarlabs . org/\
myo- sugar- activities -examples /mainline .git
This command should be typed all on one line. The backslash (\ ) character at the end
of the first line is used in Linux to continue a long command to a second line. It is used
here to make the command fit on the page of the printed version of this book. When
you type in the command you can leave it out and type myo-sugar-activities-
examples/mainline.git immediately following git.sugarlabs.org/.
This convention of splitting long commands over multiple lines will be used throughout
this book. In addition to that, the code in Git will generally have longer lines than
you'll see in the code listings in the book. For that reason I'd recommend that you not
try to type in the code from these listings, but use the code in Git instead.
The GIMP
The GIMP is one of the most useful and badly named programs ever developed. You
can think of it as a free version of Adobe Photoshop. If you need to work with image
files (other than SVG's) you need this program.
■ans-i # I |Clrc« ill>
-Sculr —( — \7Z
ty En_-.li Z v .. -i .-
I ■''PP'-Mttt*
h.reirerta
; Lsc Lckir rrcri i;r_nJcr.t
21
You may never need this program to develop the Activity itself, but when it's time to
distribute the Activity you'll use it to create screen shots of your Activity in action.
Nothing sells an Activity to a potential user like good screen shots.
Sugar Emulation
Most Linux distributions should have Sugar included. In Fedora you can run Sugar as
an alternative desktop environment. When you log in to GDM Sugar appears as a
desktop selection alongside GNOME, KDE, Window Maker, and any other window
managers you have installed.
This is not the normal way to use Sugar for testing. The normal way uses a tool called
Xephyr to run a Sugar environment in a window on your desktop. In effect, Xephyr
runs an X session inside a window and Sugar runs in that. You can easily take screen
shots of Sugar in action, stop and restart Sugar sessions without restarting the computer,
and run multiple copies of Sugar to test collaboration.
I] Applications HaMi Svstsfii ftf,' r}
$£ | ^ Aephyr -on :L&D.O [-±rl... |
I'll have more to say about this when it's time to test your first Activity.
22
6.
Creating your First Sugar Activity
Make A Standalone Python Program First
The best advice I could give a beginning Activity developer is to make a version of your
Activity that can run on its own, outside of the Sugar environment. Testing and
debugging a Python program that stands alone is faster, easier and less tedious than
doing the same thing with a similar Activity You'll understand why when you start
testing your first Activity.
The more bugs you find before you turn your code into an Activity the better. In fact,
it's a good idea to keep a standalone version of your program around even after you
have the Activity version well underway. I used my standalone version of Read Etexts
to develop the text to speech with highlighting feature. This saved me a lot of time,
which was especially important because I was figuring things out as I went.
Our first project will be a version of the Read Etexts Activity I wrote.
Inherit From The sugar.activity .Activity Class
Next we're going to take our standalone Python program and make an Activity out of
it. To do this we need to understand the concept of inheritance. In everyday speech
inheritance means getting something from your parents that you didn't work for. A
king will take his son to a castle window and say, "Someday, lad, this will all be yours!"
That's inheritance.
In the world of computers programs can have parents and inherit things from them.
Instead of inheriting property, they inherit code. There is a piece of Python code called
sugar.activity. Activity that's the best parent an Activity could hope to have, and we're
going to convince it to adopt our program. This doesn't mean that our program will
never have to work again, but it won't have to work as much.
Package The Activity
Now we have to package up our code to make it something that can be run under Sugar
and distributed as an .xo file. This involves setting up a MANIFEST, activity.info,
setup.py, and creating a suitable icon with Inkscape.
23
Add Refinements
Every Activity will have the basic Activity toolbar. For most Activities this will not be
enough, so we'll need to create some custom toolbars as well. Then we need to hook
them up to the rest of the Activity code so that what happens to the toolbar triggers
actions in the Activity and what happens outside the toolbar is reflected in the state of
the toolbar.
In addition to toolbars we'll look at some other ways to spiff up your Activity.
Put The Project Code In Version Control
By this time we'll have enough code written that it's worth protecting and sharing with
the world. To do that we need to create a Git repository and add our code to it. We'll
also go over the basics of using Git.
Going International With Pootle
Now that our code is in Git we can request help from our first collaborator: the Pootle
translation system. With a little setup work we can get volunteers to make translated
versions of our Activity available.
Distributing The Activity
In this task we'll take our Activity and set it up on http: //activities . sugarlab s . org plus
we'll package up the source code so it can be included in Linux distributions.
Add Collaboration
Next we'll add code to share e-books with Friends and the Neighborhood.
Add Text To Speech
Text to Speech with word highlighting is next. Our simple project will become a Kindle-
killer!
24
/ • A Standalone Python Program For
Reading Etexts
The Program
Our example program is based on the first Activity I wrote, Read Etexts. This is a
program for reading free e-books.
The oldest and best source of free e-books is a website called Project Gutenberg
(http://www.gutenberg.org/wiki/Main Page ). They create books in plain text format, in
other words the kind of file you could make if you typed a book into Notepad and hit
the Enter key at the end of each line. They have thousands of books that are out of
copyright, including some of the best ever written. Before you read further go to that
website and pick out a book that interests you. Check out the "Top 100" list to see the
most popular books and authors.
The program we're going to create will read books in plain text format only.
There is a Git repository containing all the code examples in this book. Once you have
Git installed you can copy the repository to your computer with this command:
git clone git : //git . sugarlabs . org/\
myo- sugar- activities -examples /mainline .git
The code for our standalone Python program will be found in the directory
Make_Standalone_Python in a file named ReadEtexts.py. It looks like this:
# ! /usr/bin/env python
import sys
import os
import zipfile
import pygtk
import gtk
import getopt
import pango
page=0
PAGE_SIZE =45
class ReadEtexts ( ) :
def keypress_cb ( self , widget, event):
"Respond when the user presses one of the arrow keys"
keyname = gtk . gdk . keyval_name (event . keyval )
if keyname == 'plus' :
self . f ont_increase ( )
25
return True
if keyname == 'minus' :
self . font_de crease ( )
return True
if keyname == ' Page_Up ' :
self . page_previous ()
return True
if keyname == ' Page_Down ' :
self . page_next ()
return True
if keyname == 'Up' or keyname == ' KP_Up ' \
or keyname == 'KP_Left' :
self . scrollup ( )
return True
if keyname == 'Down' or keyname == ' KP_Down ' \
or keyname == ' KP_Right ' :
self . scroll_down ( )
return True
return False
def page_previous (self ) :
global page
page=page-l
if page < 0: page=0
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . upper - \
v_adj ustment .page_size
def page_next ( self ) :
global page
page=page+l
if page >!= len (self .page_index) : page=0
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . lower
def f ont_decrease (self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size - 1
if font_size < 1:
font_size = 1
self . f ont_desc . set_size ( f ont_size * 1024)
self.textview . modif y_f ont (self . f ont_desc)
def f ont_increase (self ) :
font^size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size + 1
self . f ont_desc . set_size ( f ont_size * 1024)
self.textview .modif y_f ont (self . f ont_desc)
def scroll_down (self ) :
v_adjustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . upper - \
v adjustment .page size:
26
self . page_next ( )
return
if v_adj ustment . value < v_adjustment . upper -\
v_adj ustment .page_size:
new_value = v_adjustment . value + \
v_adj ustment . step_increment
if new value > v adjustment . upper -\
v_adj ustment .page_size:
new value = v adjustment . upper -\
v_adj ustment .page_size
v adj ustment . value = new value
def scroll_up ( self ) :
v_adjustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . lower :
self . page_previous ()
return
if v_adj ustment . value > v_adjustment . lower :
new value = v adjustment . value - \
v_adj ustment . step_increment
if new_value < v_adjustment . lower :
new_value = v_adjustment . lower
v adj ustment . value = new value
def show_page ( self , page_number) :
global PAGE_SIZE, current_word
position = self . page_index [pagenumber ]
self . etext_f ile . seek (position)
linecount =
label_text = '\n\n\n'
textbuffer = self . textview . get_buf f er ( )
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + Unicode (line,
'iso-8859-1' )
linecount = linecount + 1
label_text = label_text + '\n\n\n'
textbuffer. set_text (label_text )
self. textview. set_buf f er (textbuffer)
def save_extracted_f ile (self , zipfile, filename) :
"Extract the file to a temp directory for viewing'
filebytes = zipfile . read ( filename)
f = open("/tmp/" + filename, 'w')
try :
f. write (filebytes)
finally:
f . close
def read_f ile ( self , filename) :
"Read the Etext file"
global PAGE_SIZE
if zipfile . is_zipf ile ( filename) :
self.zf = zipfile . ZipFile ( filename, 'r')
self . book_f iles = self . zf . namelist ( )
self. save extracted file (self . zf,
27
self .book_files [0] )
currentFileName = "/tmp/" + self . book_f iles [ J
else :
currentFileName = filename
self . etext_f ile = open (currentFileName, "r" )
self . page_index = [ ]
linecount =
while self . etext_f ile :
line = self . etext_f ile . readline ( )
if not line :
break
linecount = linecount + 1
if linecount >= PAGE_SIZE:
position = self . etext_f ile . tell ( )
self.page_index. append (position)
linecount =
if filename.endswithC.zip") :
os . remove (currentFileName)
def destroy_cb (self , widget, data=None):
gtk .main_quit ()
def main (self, file_path) :
self. window = gtk .Window (gtk . WINDOW_TOPLEVEL)
self. window. connect ("destroy", self. destroy_cb)
self. window. set_title ( "Read E texts" )
self . window . set_size_re quest ( 640 , 480)
self. window. set_border_width (0 )
self . read_f ile ( f ile_path)
self . scrolled_window = gtk . ScrolledWindow (
hadjustment=None, vadjustment=None)
self . textview = gtk . TextView ( )
self.textview. set_edi table (False)
self. textview. set_lef t_margin (50 )
self. textview. set_cursor_visible (False)
self. textview. connect ( "key_press_event" ,
self . keypress_cb)
buffer = self . textview . get_buffer ( )
self . f ont_desc = pango . FontDescription ( "sans 12")
font_size = self . f ont_desc . get_size ( )
self. textview . modif y_f ont (self . f ont_desc)
self . show_page ( )
self . scrolled_window . add (self. textview)
self. window. add (self . scrolled_window)
self. textview. show()
self . scrolled_window . show ( )
v_adjustment = \
self . scrolled_window . get_vadjustment ( )
self.window.show()
gtk .main ( )
if name == " main " :
try:
opts, args = getopt . getopt (sys . argv [ 1 : ] , "")
ReadE texts () .main (args [0] )
except getopt . error , msg:
print msg
28
print "This program has no options"
sys . exit (2 )
Running The Program
To run the program you should first make it executable. You only need to do this once:
chmod 755 ReadEtexts . py
For this example I downloaded the file for Pride and Prejudice. The program will work
with either of the Plain text formats, which are either uncompressed text or a Zip file.
The zip file is named 1342.zip, and we can read the book by running this from a
terminal:
. /ReadEtexts . py 1342.zip
This is what the program looks like in action:
Read Etexts
Produced by Anonymous Volunteers
PRIDEAND PREJUDICE
By Jane Austen
Chapter 1
It is a truth universally acknowledged, that a single man in possession
of a good fortune, must be in want of a wife,
However little known the feelings or views of such a man may be on his
first entering a neighbourhood, this truth is so well fixed in the minds
u.
_
You can use the Page Up, Page Down, Up, Down, Left, and Right keys to navigate
through the book and the '+' and '-' keys to adjust the font size.
29
How The Program Works
This program reads through the text file containing the book and divides it into pages of
45 lines each. We need to do this because the gtk.TextView component we use for
viewing the text would need a lot of memory to scroll through the whole book and that
would hurt performance. A second reason is that we want to make reading the e-book
as much as possible like reading a regular book, and regular books have pages. If a
teacher assigns reading from a book she might say "read pages 35-50 for tommorow".
Finally, we want this program to remember what page you stopped reading on and
bring you back to that page again when you read the book next time. (The program we
have so far doesn't do that yet).
To page through the book we use random access to read the file. To understand what
random access means to a file, consider a VHS tape and a DVD. To get to a certain
scene in a VHS tape you need to go through all the scenes that came before it, in order.
Even though you do it at high speed you still have to look at all of them to find the
place you want to start watching. This is sequential access. On the other hand a DVD
has chapter stops and possibly a chapter menu. Using a chapter menu you can look at
any scene in the movie right away, and you can skip around as you like. This is
random access, and the chapter menu is like an index. Of course you can access the
material in a DVD sequentially too.
We need random access to skip to whatever page we like, and we need an index so that
we know where each page begins. We make the index by reading the entire file one line
at a time. Every 45 lines we make a note of how many characters into the file we've
gotten and store this information in a Python list. Then we go back to the beginning of
the file and display the first page. When the program user goes to the next or previous
page we figure out what the new page number will be and look in the list entry for that
page. This tells us that page starts 4,200 characters into the file. We use seek() on the
file to go to that character and then we read 45 lines starting at that point and load them
into the TextView.
When you run this program notice how fast it is. Python programs take longer to run a
line of code than a compiled language would, but in this program it doesn't matter
because the heavy lifting in the program is done by the TextView, which was created in
a compiled language. The Python parts don't do that much so the program doesn't
spend much time running them.
Sugar uses Python a lot, not just for Activities but for the Sugar environment itself. You
may read somewhere that using so much Python is "a disaster" for performance. Don't
believe it.
30
There are no slow programming languages, only slow programmers.
31
O • Inherit From sugar .activity .Activity
Object Oriented Python
Python supports two styles of programming: procedural and object oriented.
Procedural programming is when you have some input data, do some processing on it,
and produce an output. If you want to calculate all the prime numbers under a hundred
or convert a Word document into a plain text file you'll probably use the procedural
style to do that.
Object oriented programs are built up from units called objects. An object is described
as a collection of fields or attributes containing data along with methods for doing things
with that data. In addition to doing work and storing data objects can send messages to
one another.
Consider a word processing program. It doesn't have just one input, some process, and
one output. It can receive input from the keyboard, from the mouse buttons, from the
mouse traveling over something, from the clipboard, etc. It can send output to the
screen, to a file, to a printer, to the clipboard, etc. A word processor can edit several
documents at the same time too. Any program with a GUI is a natural fit for the object
oriented style of programming.
Objects are described by classes. When you create an object you are creating an instance
of a class.
There's one other thing that a class can do, which is to inherit methods and attributes
from another class. When you define a class you can say it extends some class, and by
doing that in effect your class has the functionality of the other class plus its own
functionality. The extended class becomes its parent.
All Sugar Activities extend a Python class called sugar.activity.Activity. This class
provides methods that all Activities need. In addition to that, there are methods that
you can override in your own class that the parent class will call when it needs to. For
the beginning Activity writer three methods are important:
init ()
This is called when your Activity is started up. This is where you will set up the user
interface for your Activity, including toolbars.
32
rea d_file(self, filejpa th )
This is called when you resume an Activity from a Journal entry. It is called after the
init () method is called. The file_path parameter contains the name of a temporary
file that is a copy of the file in the Journal entry. The file is deleted as soon as this
method finishes, but because Sugar runs on Linux if you open the file for reading your
program can continue to read it even after it is deleted and it the file will not actually go
away until you close it.
write_file(self, filejpa th )
This is called when the Activity updates the Journal entry. Just like with read_file() your
Activity does not work with the Journal directly Instead it opens the file named in
file_path for output and writes to it. That file in turn is copied to the Journal entry.
There are three things that can cause write_file() to be executed:
• Your Activity closes.
• Someone presses the Keep button in the Activity toolbar.
• Your Activity ceases to be the active Activity, or someone moves from the Activity
View to some other View.
In addition to updating the file in the Journal entry the read_file() and write_file()
methods are used to read and update the metadata in the Journal entry.
When we convert our standalone Python program to an Activity we'll take out much of
the code we wrote and replace it with code inherited from the sugar.activity.Activity
class.
Extending The Activity Class
Here's a version of our program that extends Activity. You'll find it in the Git repository
in the directory Inherit_From_sugar.activity.Activity under the name
ReadEtextsActivity.py:
import sys
import os
import zipfile
import pygtk
import gtk
import pango
from sugar . activity import activity
from sugar . graphics import style
33
page=0
PAGE_SIZE =45
class ReadEtextsActivity (activity .Activity) :
def init (self, handle) :
"The entry point to the Activity"
global page
activity. Activity . init (self, handle)
toolbox = activity . ActivityToolbox (self )
activity_toolbar = toolbox . get_activity_toolbar ( )
activity_toolbar . keep .props .visible = False
activity_toolbar . share .props .visible = False
self . set_toolbox (toolbox)
toolbox . show ( )
self . scrolled_window = gtk . ScrolledWindow ( )
self. scrolled_window. set_policy (gtk. POLICY_NEVER,
gtk. POLICY_AUTOMATIC)
self . scrolled_window .props . shadow_type = \
gtk.SHADOW_NONE
self . textview = gtk . TextView ( )
self .textview. set_edi table (False)
self. textview. set_cursor_visible (False)
self. textview. set_lef t_margin (50 )
self. textview. connect ( "key_press_event" ,
self . keypress_cb)
self . scrolled_window . add (self. textview)
self . set_canvas (self . scrolled_window)
self. textview. show()
self . scrolled_window . show ( )
page =
self . textview . grab_focus ()
self . f ont_desc = pango . FontDescription ( "sans %d" %
style . zoom ( 10 ) )
self. textview . modif y_f ont (self . f ont_desc)
def keypress_cb (self , widget, event):
"Respond when the user presses one of the arrow keys"
keyname = gtk . gdk . keyval_name (event . keyval)
print keyname
if keyname == 'plus':
self . f ont_increase ( )
return True
if keyname == 'minus' :
self . font_de crease ( )
return True
if keyname == ' Page_Up ' :
self . page_previous ()
return True
if keyname == 'PageDown' :
self . page_next ()
return True
if keyname == 'Up' or keyname == ' KP_Up ' \
or keyname == 'KP_Left' :
self . scrollup ( )
34
return True
if keyname == 'Down' or keyname == ' KP_Down ' \
or keyname == 'KP_Right' :
self . scroll_down ( )
return True
return False
def page_previous ( self ) :
global page
page=page-l
if page < 0: page=0
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadjustment ( )
v_adj ustment . value = v_adjustment . upper -\
v_adj ustment .page_size
def page_next ( self ) :
global page
page=page+l
if page >= len ( self .page_index) : page=0
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . lower
def f ont_decrease ( self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size - 1
if font_size < 1:
font_size = 1
self . f ont_desc . set_size ( f ont_size * 1024)
self.textview . modif y_f ont (self . f ont_desc)
def f ont_increase ( self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size + 1
self . f ont_desc . set_size ( f ontsize * 1024)
self.textview .modif y_f ont (self . f ont_desc)
def scrolldown ( self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . upper - \
v_adj ustment .page_size:
self . page_next ( )
return
if v_adj ustment . value < v_adjustment . upper -\
v_adj ustment .page_size:
new_value = v_adjustment . value +\
v_adj ustment . step_increment
if new_value > v_adjustment . upper -\
v_adj ustment .page_size:
new value = v adjustment . upper -\
v_adj ustment .page_size
v adj ustment . value = new value
def scroll_up ( self ) :
35
v_adjustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . lower :
self . page_previous ()
return
if v_adj ustment . value > v_adjustment . lower :
new value = v adjustment .value - \
v_adjustment . step_increment
if new_value < v_adjustment . lower :
new value = v adjustment . lower
v adj ustment . value = new value
def show_page ( self , page_number) :
global PAGE_SIZE, current_word
position = self .page_index [page_number ]
self . etext_f ile .seek (position)
linecount =
label_text = '\n\n\n'
textbuffer = self . textview . get_buffer ( )
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + Unicode (line,
'iso-8859-1' )
linecount = linecount + 1
label_text = label_text + '\n\n\n'
textbuffer. set_text (label_text )
self. textview. setbuffer (textbuffer)
def save_extracted_f ile (self , zipfile, filename):
"Extract the file to a temp directory for viewing'
filebytes = zipfile . read ( filename)
outfn = self .make_new_f ilename (filename)
if (outfn == ' ' ) :
return False
f = open (os .path . j oin (self . get_activity_root () ,
'instance', outfn), 'w')
try:
f. write (filebytes)
finally:
f . close
def read_f ile ( self , filename) :
"Read the Etext file"
global PAGE_SIZE
if zipfile . is_zipf ile ( filename) :
self.zf = zipfile . ZipFile (filename, 'r')
self . book_f iles = self . zf . namelist ( )
self . save_ext r act ed_ file (self . zf ,
self .book_filesT0] )
currentFileName = os .path . j oin (
self . get_activity_root () ,
'instance', self .book_f iles [ ] )
else :
currentFileName = filename
self . etext_f ile = open (currentFileName, "r" )
self . page_index = [ ]
36
linecount =
while self . etext_f ile :
line = self . etext_f ile . readline ( )
if not line:
break
linecount = linecount + 1
if linecount >= PAGE_SIZE:
position = self . etext_f ile . tell ( )
self . page_index . append (position)
linecount =
if filename.endswithC.zip") :
os . remove (currentFileName)
self . show_page ( )
def make_new_f ilename ( self , filename) :
partition_tuple = f ilename . rpartition ('/' )
return partition tuple [2]
This program has some significant differences from the standalone version. First, note
that this line:
# ! /usr/bin/env python
has been removed. We are no longer running the program directly from the Python
interpreter. Now Sugar is running it as an Activity. Notice that much (but not all) of
what was in the main() method has been moved to the init () method and the
mainO method has been removed.
Notice too that the class statement has changed:
class Re a dEtextsActivity (activity. Activity)
This statement now tells us that class ReadEtextsActivity extends the class
sugar.activity.Activity. As a result it inherits the code that is in that class. Therefore
we no longer need a GTK main loop, or to define a window. The code in this class we
extend will do that for us.
While we gain much from this inheritance, we lose something too: a title bar for the
main window. In a graphical operating environment a piece of software called a window
manager is responsible for putting borders on windows, making them resizeable,
reducing them to icons, maximizing them, etc. Sugar uses a window manager named
Matchbox which makes each window fill the whole screen and puts no border, title bar,
or any other window decorations on the windows. As a result of that we can't close our
application by clicking on the "X" in the title bar as before. To make up for this we need
to have a toolbar that contains a dose button. Thus every Activity has an Activity
toolbar that contains some standard controls and buttons. If you look at the code you'll
see I'm hiding a couple of controls which we have no use for yet.
37
The read_file() method is no longer called from the main() method and doesn't seem to
be called from anywhere in the program. Of course it does get called, by some of the
Activity code we inherited from our new parent class. Similarly the init () and
write _file() methods (if we had a write_file() method) get called by the parent Activity
class.
If you're especially observant you might have noticed another change. Our original
standalone program created a temporary file when it needed to extract something from
a Zip file. It put that file in a directory called /tmp. Our new Activity still creates the file
but puts it in a different directory, one specific to the Activity.
All writing to the file system is restricted to subdirectories of the path given by
self.get_activity _root(). This method will give you a directory that belongs to your
Activity alone. It will contain three subdirectories with different policies:
data
This directory is used for data such as configuration files. Files stored here will
survive reboots and OS upgrades.
tmp
This directory is used similar to the /tmp directory, being backed by RAM. It may
be as small as 1 MB. This directory is deleted when the activity exits.
instance
This directory is similar to the tmp directory, being backed by the computer's drive
rather than by RAM. It is unique per instance. It is used for transfer to and from
the Journal. This directory is deleted when the activity exits.
Making these changes to the code is not enough to make our program an Activity. We
have to do some packaging work and get it set up to run from the Sugar emulator. We
also need to learn how to run the Sugar emulator. That comes next!
38
Zs • Package The Activity
Add setup.py
You'll need to add a Python program called setup.py to the same directory that you
Activity program is in. Every setup.py is exactly the same as every other setup.py.
The copies in our Git repository look like this:
# ! /usr/bin/env python
# Copyright (C) 2006, Red Hat, Inc.
#
# This program is free software; you can redistribute it
# and/or modify it under the terms of the GNU General
# Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will
# be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General
# Public License along with this program; if not,
# write to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301 USA
from sugar . activity import bundlebuilder
bundlebuilder . start ( )
Be sure and copy the entire text above, including the comments.
The setup.py program is used by sugar for a number of purposes. If you run setup.py
from the command line you'll see the options that are used with it and what they do.
[jim@simmons bookexamples ] $ ./setup.py
/usr/lib/python2 .6/site-packages/sugar/util.py:25:
DeprecationWarning : the sha module is deprecated;
use the hashlib module instead
import sha
Available commands:
build Build generated files
dev Setup for development
dist_xo Create a xo bundle package
dist_source Create a tar source package
fix manifest Add missing files to the manifest
39
genpot Generate the gettext pot file
install Install the activity in the system
(Type ". /setup. py <command> --help" for help about a
particular command's options.
We'll be running some of these commands later on. Don't be concerned about the
DeprecationWarning message. That is just Python's way of telling us that it has a
new way of doing something that is better but the old way we are using still works.
The error is coming from code in Sugar itself and should be fixed in some future Sugar
release.
Create activity.info
Next create a directory within the one your progam is in and name it activity. Create a
file named activity.info within that directory and enter the lines below into it. Here is
the one for our first Activity:
[Activity]
name = Read ETexts II
service_name = net . f lossmanuals . ReadEtextsActivity
icon = read-etexts
exec = sugar-activity ReadEtextsActivity . ReadEtextsActivity
show_launcher = no
activity version = 1
mime_types = text/plain; application/zip
license = GPLv2+
This file tells Sugar how to run your Activity. The properties needed in this file are:
name The name of your Activity as it will appear to the user.
A unique name that Sugar will use to refer to your Activity. Any Journal entry created
service_name by your Activity will have this name stored in its metadata, so that when someone
resumes the Journal entry Sugar knows to use the program that created it to read it.
icon The name of the icon file you have created for the Activity. Since icons are always .svg
files the icon file in the example is named read-etexts.svg.
exec This tells Sugar how to launch your Activity. What it says is to create an instance of the
class ReadEtextsActivity which it will find in file ReadEtextsActivity.py.
show_launcher There are two ways to launch an Activity. The first is to click on the icon in the Activity
view. The second is to resume an entry in the Journal. Activities that don't create Journal
entries can only be resumed from the Journal, so there is no point in putting an icon in
the Activity ring for them. Read Etexts is an Activity like that.
activity_version An integer that represents the version number of your program. The first version is 1,
the next is 2, and so on.
mime_types Generally when you resume a Journal entry it launches the Activity that created it. In
the case of an e-book it wasn't created by any Activity, so we need another way to tell
the Journal which Activity it can use. A MIME type is the name of a common file
40
format. Some examples are text/plain, text/html, application/zip and application/pdf.
In this entry we're telling the Journal that our program can handle either plain text files
or Zip archive files.
license Owning a computer program is not like buying a car. With a car, you're the owner and
you can do what you like with it. You can sell it, rent it out, make it into a hot rod,
whatever. With a computer program there is always a license that tells the person
receiving the program what he is allowed to do with it. GPLv2+ is a popular standard
license that can be used for Activities, and since this is my program that is what goes
here. When you're ready to distribute one of your Activities I'll have more to say about
licenses.
Create An Icon
Next we need to create an icon named read-etexts.svg and put it in the activity
subdirectory. ! We're going to use Inkscape to create the icon. From the New menu in
Inkscape select icon 48x48. This will create a drawing area that is a good size.
You don't need to be an expert in Inkscape to create an icon. In fact the less fancy your
icon is the better. When drawing your icon remember the following points:
• Your icon needs to look good in sizes ranging from really, really small to large.
• It needs to be recognizeable when its really, really small.
• You only get to use two colors: a stroke color and a fill color. It doesn't matter
which ones you choose because Sugar will need to override your choices anyway,
so just use black strokes on a white background.
• A fill color is only applied to an area that is contained within an unbroken stroke.
If you draw a box and one of the corners doesn't quite connect the area inside that
box will not be filled. Free hand drawing is only for the talented. Circles, boxes,
and arcs are easy to draw with Inkscape so use them when you can.
• Inkscape will also draw 3D boxes using two point perspective. Don't use them.
Icons should be flat images. 3D just doesn't look good in an icon.
• Coming up with good ideas for icons is tough. I once came up with a rather nice
picture of a library card catalog drawer for Get Internet Archive Books. The
problem is, no child under the age of forty has ever seen a card catalog and fewer
still understand its purpose.
When you're done making your icon you need to modify it so it can work with Sugar.
Specifically, you need to make it show Sugar can use its own choice of stroke color and
fill color. The SVG file format is based on XML, which means it is a text file with some
special tags in it. This means that once we have finished editing it in Inkscape we can
load the file into Eric and edit it as a text file.
41
I'm not going to put the entire file in this chapter because most of it you'll just leave
alone. The first part you need to modify is at the very beginning.
Before:
<?xml version="l . " encoding="UTF-8" standalone="no" ?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
After:
<?xml version="l . 0" ?>
<!D0CTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN'
'http: // www. w3.org/ Graphics/ SVG/ 1 . 1/ DTD /svg 11 . dtd' [
<!ENTITY stroke_color "#000000">
<!ENTITY fill_color "#FFFFFF">
] Xsvg
Now in the body of the document you'll find references to fill and stroke as part of an
attribute called style. Every line or shape you draw will have these, like this:
<rect
style="fill:#ffffff;stroke:#000000; stroke-opacity: 1"
id="rect904"
width="36. 142857"
height="32 . 142857"
x="4 . 1428571"
y="7 . 1428571" />
You need to change each one to look like this:
<rect
style="fill:&fill_color;; stroke: &stroke_color ;
; stroke-opacity: 1 "
id="rect904"
width="36. 142857"
height="32 . 142857"
x="4 . 1428571"
y="7 . 1428571" />
Note that & 'stroke _color; and &fill_color; both end with semicolons (;), and semicolons are
also used to separate the properties for style. Because of this it is an extremely common
beginner's mistake to leave off the trailing semicolon because two semicolons in a row
don't look right. Be assured that the two semicolons in a row are intentional and
absolutely necessary! Second, the value for style should all go on one line. We split it
here to make it fit on the printed page; do not split it in your own icon!
Make a MANIFEST File
You should remember that setup.py has an option to update a manifest. Let's try it:
42
. /setup. py f ix_manif est
/usr/lib/python2 .6/site-packages/sugar/util.py:25:
DeprecationWarning : the sha module is deprecated;
use the hashlib module instead
import sha
WARNING : root :Missing po/ dir, cannot build_locale
WARNING: root :Activity directory lacks a MANIFEST file.
This actually will build a MANIFEST file containing everything in the directory and its
subdirectories. The /po directory it is complaining about is used to translate Activities
into different languages. We can ignore that for now.
The MANIFEST file it creates will contain some extra stuff, so we need to get rid of the
extra lines using Eric. The corrected MANIFEST should look like this:
setup . py
Re adE texts Activity . py
activity/read- etexts.svg
activity/ activity. info
Install The Activity
There's just one more thing to do before we can test our Activity under the Sugar
emulator. We need to install it, which in this case means making a symbolic link
between the directory we're using for our code in the -/Activities/ directory. The
symbol ~ refers to the "home" directory of the user we're running Sugar under, and a
symbolic link is a way to make a file or directory appear to be located in more than one
place without copying it. We make this symbolic link by running setup. py again:
. /setup . py dev
Running Our Activity
Now at last we can run our Activity under Sugar. To do that we need to learn how to
run sugar-emulator.
Fedora doesn't make a menu option for Sugar Emulator, but it's easy to add one
yourself. The command to run is simply
sugar-emulator
If your screen resolution is smaller than the default size sugar-emulator runs at it will
run full screen. This is not convenient for testing, so you may want to specify your own
size:
sugar-emulator -i 800x600
43
Note that this option only exists in Fedora 11 and later.
When you run sugar-emulator a window opens up and the Sugar environment starts up
and runs inside it. It looks like this:
•' |imb Slitinani Hflt'fiE ^
Dec 15. 9:35- PH
m n M
O
*
H
i® S
EJ3
33 | -^ Xnntwr wv:LMuQ fcteU |
HD = G
When running sugar-emulator you may find that some keys don't seem to work in the
Sugar environment. This is caused by bugs in the Xephyr software that creates the
window that Sugar runs in. Sometimes it has difficulty identifying your keyboard and
as a result some keys get misinterpreted. On Fedora 111 noticed that my function keys
did not work, and my regular arrow keys didn't work either although my keypad arrow
keys did. I was able to get my function keys working again by putting this line in
~ I. sugar I debug:
run setxkbmap <keymap name>
This needs more explanation. First, the symbol "~" refers to your home directory.
Second, any file named starting with a period is considered hidden in Linux, so you'll
need to use the option to show hidden files and directories in the GNOME directory
browser to navigate to it. Finally, the keymap name is a two character country code: us
for the United States, fr for France, de for Germany, etc.
44
To test our Activity we're going to need to have a book in the Journal, so use the
Browse Activity to visit Project Gutenberg again and download the book of your
choice. This time it's important to download the book in Zip format, because Browse
cannot download a plain text file to the Journal. Instead, it opens the file for viewing as
if it was a web page. If you try the same thing with the Zip file it will create an entry in
the Journal.
We can't just open the file with one click in the Journal because our program did not
create the Journal entry and there are several Activities that support the MIME type of
the Journal entry. We need to use the Start With menu option like this:
When we do open the Journal entry this is what we see:
45
Produced by Anonymous Volunteers
PRIDE AND PREJUDICE
By Jane Austen
Chapter 1
It is a truth universally acknowledged, that a single man in possession
of a good fortune, must be in want of a wife.
However little known the feelings or views of such a man may be on his
first entering a neighbourhood, this truth is so well fixed in the minds
Technically, this is the first iteration of our Activity. (Iteration is a vastly useful word
meaning something you do more than once. In this book we're building our Activity a
bit at a time so I can demonstrate Activity writing principles, but actually building a
program in pieces, testing it, getting feedback, and building a bit more can be a highly
productive way of creating software. Using the word iteration to describe each step in
the process makes the process sound more formal than it really is).
While this Activity might be good enough to show your own mother, we really should
improve it a bit before we do that. That part comes next.
46
-L U • Add Refinements
Toolbars
It is a truth universally acknowledged that a first rate Activity needs good Toolbars. In
this chapter we'll learn how to make them. We're going to put the toolbar classes in a
separate file from the rest, because there are two styles of toolbar (old and new) and we
may want to support both in our Activity. If we have two different files containing
toolbar classes our code can decide at runtime which one it wants to use. For now, this
code supports the old style, which works with every version of Sugar. The new style is
currently only supported by Sugar on a Stick.
There is a file called toolbar.py in the Add_Refinements directory of the Git
repository that looks like this:
from gettext import gettext as
import re
import pango
import gobject
import gtk
from sugar . graphics . toolbutton import ToolButton
from sugar . activity import activity
class ReadToolbar (gtk . Toolbar ) :
gtype_name = 'ReadToolbar'
def init (self):
gtk. Toolbar . init (self)
self. back = ToolButton (' go-previous ' )
self. back. set_tooltip (_ ( ' Back ' ) )
self . back . props . sensitive = False
self . insert ( self . back, -1)
self. back. show ( )
self. forward = ToolButton (' go-next ' )
self. forward. set_tooltip (_( 'Forward'))
self . forward. props . sensitive = False
self . insert ( self . forward, -1)
self. forward. show ()
num_page_item = gtk . Toolltem ( )
self . num_page_entry = gtk. Entry ()
self . num_page_entry . set_text ( ' ' )
self . num_page_entry . set_alignment ( 1 )
self . num_page_entry .connect ( 'insert-text' ,
47
self . num_page_entry_insert_text_cb)
self . num_page_entry . set_width_chars (4 )
num_page_item. add (self . num_page_entry)
self . num_page_entry . show ( )
self . insert (num_page_item, -1)
num_page_item. show ( )
total_page_item = gtk . Toolltem ( )
self . total_page_label = gtk. Label ()
label_attributes = pango . AttrList ( )
label_attributes .insert (pango . AttrSize (
14000, 0, -1))
label_attributes .insert (pango. At tr Fore ground (
65535, 65535, 65535, 0, -1))
self . total_page_label . set_at tributes (
label_at tributes )
self . total_page_label . set_text ( ' / 0')
total_page_item. add (self . total_page_label )
self . total_page_label . show ( )
self . insert (total_page_item, -1)
total_page_item. show ( )
def num_page_entry_insert_text_cb (self , entry, text,
length, position) :
if not re. match (' [0-9] ' , text):
entry. emit_stop_by_name ('insert-text')
return True
return False
def update_nav_buttons (self ) :
current page = self. current page
self . back . props . sensitive = current_page >
self . forward. props . sensitive = \
current_page < self . total_pages - 1
self . num_page_entry .props . text = str (
current_page + 1)
self . total_page_label .props . label = \
' / ' + str (self . total_pages )
def set_total_pages (self , pages):
self . total_pages = pages
def set_current_page (self , page):
self . current_page = page
self . update_nav_buttons ( )
class ViewToolbar (gtk . Toolbar ) :
gtype_name = 'ViewToolbar'
gsignals = {
48
'needs-update-size ' : (gob j ect . SIGNAL_RUN_FIRST,
gobject .TYPE_NONE,
([])),
'go-fullscreen' : (gob j ect . SIGNAL_RUN_FIRST,
gobject .TYPE_NONE,
( [] ) )
}
def init (self):
gtk. Toolbar . init (self)
self . zoom_out = ToolButton (' zoom-out ' )
self . zoom_out . set_tooltip (_ ( ' Zoom out ' ) )
self . insert ( self . zoom_out, -1)
self . zoom_out . show ( )
self .zoora_in = ToolButton (' zoom-in ' )
self . zoom_in . set_tooltip (_ ( ' Zoom in ' ) )
self . insert ( self . zoom_in, -1)
self . zoom_in . show ( )
spacer = gtk . SeparatorToolItem ( )
spacer . props . draw = False
self . insert ( spacer , -1)
spacer . show ( )
self . fullscreen = ToolButton (' view-fullscreen ' )
self. fullscreen. set_tooltip (_( 'Fullscreen' ) )
self. fullscreen. connect ( 'clicked' ,
self . f ullscreen_cb)
self . insert ( self . fullscreen, -1)
self. fullscreen. show ()
def f ullscreen_cb ( self , button):
self . emit ( 'go-fullscreen' )
Another file in the same directory of the Git repository is named
ReadEtextsActivity2.py. It looks like this:
import os
import zipfile
import gtk
import pango
from sugar . activity import activity
from sugar . graphics import style
from toolbar import ReadToolbar, ViewToolbar
from gettext import gettext as _
page=0
PAGE_SIZE =45
TOOLBAR_READ = 2
class ReadEtextsActivity (activity .Activity) :
def init (self, handle) :
"The entry point to the Activity"
global page
activity .Activity . init (self, handle)
toolbox = activity . ActivityToolbox (self )
49
activity_toolbar = toolbox . get_activity_toolbar ( )
activity_toolbar . keep .props .visible = False
activity_toolbar . share .props .visible = False
self . edit_toolbar = activity . EditToolbar ( )
self . edit_toolbar . undo .props . visible = False
self . edit_toolbar . redo .props . visible = False
self . edit_toolbar . separator .props . visible = False
self . edit_toolbar . copy . set_sensitive (False)
self . edit_toolbar .copy. connect ( 'clicked' ,
self . edit_toolbar_copy_cb)
self . edit_toolbar .paste .props . visible = False
toolbox . add_toolbar (_ ( ' Edit ' ) , self. edit_toolbar )
self . edit_toolbar . show ( )
self . read_toolbar = ReadToolbar ( )
toolbox . add_toolbar (_ ( ' Read 1 ) , self. read_toolbar )
self . readtoolbar .back. connect ( 'clicked' ,
self . go_back_cb)
self . read_toolbar . forward. connect ( 'clicked' ,
self . go_f orward_cb)
self . read_toolbar . num_page_entry .connect ( 'activate' ,
self . num_page_entry_activate_cb)
self . read_toolbar . show ( )
self . view_toolbar = ViewToolbar ( )
toolbox . add_toolbar (_ ( ' View ' ) , self. view_toolbar )
self . view_toolbar .connect ( 'go-fullscreen' ,
self . view_toolbar_go_f ullscreen_cb)
self . view_toolbar . zoom_in .connect ( 'clicked' ,
self . zoom_in_cb)
self . view_toolbar . zoom_out .connect ( 'clicked' ,
self . zoom_out_cb)
self . view_toolbar . show ( )
self . set_toolbox (toolbox)
toolbox . show ( )
self . scrolled_window = gtk . ScrolledWindow ( )
self. scrolled_window. set_policy (gtk. POLICY_NEVER,
gtk. POLICY_AUTOMATIC)
self . scrolled_window .props . shadow_type = \
gtk.SHADOW_NONE
self . textview = gtk . TextView ( )
self.textview. set_edi table (False)
self. textview. set_cursor_visible (False)
self. textview. set_lef t_margin (50 )
self. textview. connect ( "key_press_event" ,
self . keypress_cb)
self . scrolled_window . add (self. textview)
self . set_canvas (self . scrolled_window)
self . textview . show ( )
self . scrolled_window . show ( )
page =
self . clipboard = gtk . Clipboard (
display=gtk . gdk . display_get_def ault () ,
selection=" CLIPBOARD")
50
self . textview . grab_f ocus ()
self . f ont_desc = pango . FontDescription ( "sans %d" %
style . zoom ( 10 ) )
self. textview . modif y_f ont (self . f ont_desc)
buffer = self . textview . get_buffer ( )
self .markset_id = buff er . connect ( "mark-set" ,
self . mark_set_cb)
self. toolbox. set_current_toolbar (TOOLBAR_READ)
def keypress_cb ( self , widget, event):
"Respond when the user presses one of the arrow keys"
keyname = gtk . gdk . keyval_name (event . keyval)
print keyname
if keyname == 'plus' :
self . f ont_increase ( )
return True
if keyname == 'minus' :
self . f on t_de crease ( )
return True
if keyname == ' Page_Up ' :
self . page_previous ()
return True
if keyname == ' Page_Down ' :
self . page_next ( )
return True
if keyname == 'Up' or keyname == ' KP_Up ' \
or keyname == 'KP_Left' :
self . scroll_up ( )
return True
if keyname == 'Down' or keyname == ' KP_Down ' \
or keyname == 'KP_Right' :
self . scroll_down ( )
return True
return False
def num_page_entry_activate_cb (self , entry):
global page
if entry . props . text :
new_page = int (entry .props . text ) - 1
else :
new_page =
if new_page >= self . readtoolbar . total_pages :
new_page = self . readtoolbar . total_pages - 1
elif new_page < 0:
new_page =
self . read_toolbar . current_page = newpage
self . read_toolbar . set_current_page (new_page)
self . show_page (new_page)
entry . props . text = str (newpage + 1)
self . read_toolbar . update_nav_buttons ( )
page = new_page
def go_back_cb ( self , button) :
self . page_previous ()
51
def go_f orward_cb (self , button):
self . page_next ()
def page_previous (self ) :
global page
page=page-l
if page < 0: page=0
self . readtoolbar . set_current_page (page)
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adjustment . value = v_adj ustment . upper -\
v_adj ustment .page_size
def page_next ( self ) :
global page
page=page+l
if page >= len (self .page_index) : page=0
self . readtoolbar . set_current_page (page)
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . lower
def zoom_in_cb (self , button) :
self . f ont_increase ( )
def zoom_out_cb (self , button) :
self . f on t_de crease ( )
def f ont_decrease (self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size - 1
if font_size < 1:
font_size = 1
self . f ont_desc . set_size ( f ont_size * 1024)
self .textview . modif y_f ont (self . f ont_desc)
def f ont_increase (self ) :
font^size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size + 1
self . f ont_desc . set_size ( f ont_size * 1024)
self. textview .modif y_f ont (self . f ont_desc)
def mark_set_cb (self , textbuffer, iter, textmark) :
if textbuf f er . get_has_selection ( ) :
begin, end = textbuf fer . get_selection_bounds ( )
self . edit_toolbar . copy . set_sensitive (True)
else :
self . edit_toolbar . copy . set_sensitive (False)
def edit_toolbar_copy_cb (self , button):
textbuffer = self . textview . get_buf fer ( )
begin, end = textbuf fer . get_selection_bounds ( )
copy_text = textbuf fer . get_text (begin, end)
self. clipboard. set_text (copy_text )
52
def view_toolbar_go_f ullscreen_cb (self , view_toolbar ) :
self. fullscreen ()
def scrolldown ( self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . upper - \
v_adj ustment .page_size :
self . page_next ( )
return
if v_adj ustment . value < v_adjustment . upper - \
v_adj ustment .page_size:
new_value = v_adjustment . value + \
v_adj ustment . step_increment
if new_value > v_adjustment . upper - \
v_adj ustment .page_size:
new value = v adjustment . upper - \
v_adj ustment .page_size
v adj ustment . value = new value
def scroll_up ( self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . lower :
self . page_previous ()
return
if v_adj ustment . value > v_adjustment . lower :
new value = v adjustment . value - \
v_adj ustment . step_increment
if new_value < v_adjustment . lower :
new_value = v_adjustment . lower
v adj ustment . value = new value
def show_page ( self , pagenumber) :
global PAGE_SIZE, current_word
position = self . page_index [pagenumber ]
self . etext_f ile . seek (position)
linecount =
label_text = '\n\n\n'
textbuffer = self . textview . get_buf f er ( )
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + Unicode (line,
'iso-8859-1')
linecount = linecount + 1
label_text = label_text + '\n\n\n'
textbuffer. set_text (label_text )
self. textview. set_buf f er (textbuffer)
def save_extracted_f ile (self , zipfile, filename) :
"Extract the file to a temp directory for viewing'
filebytes = zipfile . read ( filename)
outfn = self .make_new_f ilename ( filename)
if (outfn == ' ' ) :
return False
f = open (os .path. join (self . get_activity_root () ,
' tmp ' , outfn) , ' w ' )
try :
53
f. write (filebytes)
finally:
f . close ( )
def get_saved_page_number (self ) :
global page
title = self .metadata . get (' title ' , '')
if title == ' ' or not title [ len (title) - l].isdigit()
page =
else :
i = len(title) - 1
newPage = ' '
while (title[i] .isdigit() and i > 0) :
newPage = title [i] + newPage
i = i - 1
if title [i] == ' P' :
page = int (newPage) - 1
else :
# not a page number; maybe a volume number,
page =
def save_page_number (self ) :
global page
title = self .metadata . get (' title ' , '')
if title == '' or not title [ len (title) -1 ]. isdigit () :
title = title + ' P' + str(page + 1)
else :
i = len(title) - 1
while (title [i] .isdigit() and i > 0) :
i = i - 1
if title [i] == ' P' :
title = title[0:i] + 'P' + str (page + 1)
else :
title = title + ' P' + str(page + 1)
self .metadata [' title ' ] = title
def read_f ile ( self , filename) :
"Read the Etext file"
global PAGE_SIZE, page
if zipf ile . is_zipf ile ( filename) :
self.zf = zipf ile . ZipFile (filename, 'r')
self . book_f iles = self . zf . namelist ( )
self . save_ext r act ed_ file (self . zf ,
self .book_filesT0] )
currentFileName = os .path . j oin (
self . get_activity_root () ,
' tmp ' , self .book_f iles [ ] )
else :
currentFileName = filename
self . etext_f ile = open (currentFileName, "r" )
self . page_index = [ ]
pagecount =
linecount =
while self . etext_f ile :
line = self . etext_f ile . readline ( )
if not line:
54
break
linecount = linecount + 1
if linecount >= PAGE_SIZE:
position = self . etext_file . tell ( )
self . page_index . append (position)
linecount =
pagecount = pagecount + 1
if filename.endswithC.zip") :
os . remove (currentFileName)
self . get_saved_page_number ( )
self . show_page (page)
self . read_toolbar . set_total_pages (pagecount + 1)
self . read_toolbar . set_current_page (page)
def make_new_f ilename ( self , filename) :
partition_tuple = f ilename . rpartition ('/' )
return partition tuple [2]
def write_f ile (self , filename) :
"Save meta data for the file."
self .metadata [' activity ' ] = self . get_bundle_id ( )
self . save_page_number ( )
This is the activity.info for this example:
[Activity]
name = Read ETexts II
service_name = net . f lossmanuals . ReadEtextsActivity
icon = read-etexts
exec = sugar-activity ReadEtextsActivity2 .ReadEtextsActivity
show_launcher = no
activity version = 1
mime_types = text/plain; application/zip
license = GPLv2+
The line in bold is the only one that needs changing. When we run this new version
this is what we'll see:
55
Produced by Anonymous Volunteers
PRIDE AND PREJUDICE
By Jane Austen
Chapter 1
It is a truth universally acknowledged, that a single man in possession
of a good fortune, must be in want of a wife.
However little known the feelings or views of such a man may be on his
first entering a neighbourhood, this truth is so well fixed in the minds
There are a few things worth pointing out in this code. First, have a look at this import:
from gettext import gettext as _
We'll be using the gettext module of Python to support translating our Activity into
other languages. We'll be using it in statements like this one:
self. back. set_tooltip (_ ( ' Back ' ) )
The underscore acts the same way as the gettext function because of the way we
imported gettext. The effect of this statement will be to look in a special translation file
for a word or phrase that matches the key "Back" and replace it with its translation. If
there is no translation file for the language we want then it will simply use the word
"Back". We'll explore setting up these translation files later, but for now using gettext for
all of the words and phrases we will show to our Activity users lays some important
groundwork.
56
The second thing worth pointing out is that while our revised Activity has four toolbars
we only had to create two of them. The other two, Activity and Edit, are part of the
Sugar Python library. We can use those toolbars as is, hide the controls we don't need,
or even extend them by adding new controls. In the example we're hiding the Keep
and Share controls of the Activity toolbar and the Undo, Redo, and Paste buttons of
the Edit toolbar. We currently do not support sharing books or modifying the text in
books so these controls are not needed. Note too that the Activity toolbar is part of the
ActivityToolbox. There is no way to give your Activity a toolbox that does not contain
the Activity toolbar as its first entry.
Another thing to notice is that the Activity class doesn't just provide us with a window.
The window has a VBox to hold our toolbars and the body of our Activity. We install
the toolbox using set_toolbox() and the body of the Activity using set_canvas().
The Read and View toolbars are regular PyGtk programming, but notice that there is a
special button for Sugar toolbars that can have a tooltip attached to it, plus the View
toolbar has code to hide the toolbox and ReadEtextsActivity2 has code to unhide it.
This is an easy function to add to your own Activities and many games and other kinds
of Activities can benefit from the increased screen area you get when you hide the
toolbox.
Metadata And Journal Entries
Every Journal entry represents a single file plus metadata, or information describing
the file. There are standard metadata entries that all Journal entries have and you can
also create your own custom metadata.
Unlike ReadEtextsActivity, this version has a write _file() method.
def write_f ile (self , filename) :
"Save meta data for the file."
self .metadata [' activity ' ] = self . get_bundle_id ( )
self . save_page_number ( )
We didn't have a write_file() method before because we weren't going to update the file
the book is in, and we still aren't. We will, however, be updating the metadata for the
Journal entry. Specifically, we'll be doing two things:
• Save the page number our Activity user stopped reading on so when he launches
the Activity again we can return to that page.
• Tell the Journal entry that it belongs to our Activity, so that in the future it will use
our Activity's icon and can launch our Activity with one click.
The way the Read Activity saves page number is to use a custom metadata property.
57
self .metadata [' Readcurrentpage ' ] = \
str (self ._document . get_page_cache ( ) . get_current_page ( ) )
Read creates a custom metadata property named Read_current_page to store the current
page number. You can create any number of custom metadata properties just this
easily, so you may wonder why we aren't doing that with Read Etexts. Actually, the
first version of Read Etexts did use a custom property but in Sugar .82 or lower there
was a bug in the Journal such that custom metadata did not survive after the computer
was turned off. As a result my Activity would remember pages numbers while the
computer was running, but would forget them as soon as it was shut down. XO laptops
currently cannot upgrade to anything newer than .82, and when it is possible to
upgrade it will be a big job for the schools.
To get around this problem I created the following two methods:
def get_saved_page_number (self ) :
global page
title = self .metadata . get (' title ' , '')
if title == '' or not title [ len (title) -1 ]. isdigit () :
page =
else :
i = len(title) - 1
newPage = ' '
while (title[i] .isdigit() and i > 0) :
newPage = title [i] + newPage
i = i - 1
if title [i] == ' P' :
page = int (newPage) - 1
else :
# not a page number; maybe a volume number,
page =
def save_page_number (self ) :
global page
title = self .metadata . get (' title ' , '')
if title == '' or not title [ len (title) -1 ]. isdigit () :
title = title + ' P' + str(page + 1)
else :
i = len(title) - 1
while (title[i] .isdigit() and i > 0) :
i = i - 1
if title [i] == ' P' :
title = title[0:i] + 'P' + str (page + 1)
else :
title = title + ' P' + str(page + 1)
self .metadata [' title ' ] = title
save_page_number() looks at the current title metadata and either adds a page number to
the end of it or updates the page number already there. Since title is standard metadata
for all Journal entries the Journal bug does not affect it.
58
These examples show how to read metadata too.
title = self .metadata . get (' title ' , '')
This line of code says "Get the metadata property named title and put it in the variable
named title, If there is no title property put an empty string in title.
Generally you will save metadata in the write_file() method and read it in the read_file()
method.
In a normal Activity that writes out a file in write_file() this next line would be
unnecessary:
self .metadata [' activity ' ] = self . get_bundle_id ( )
Any Journal entry created by an Activity will automatically have this property set. In
the case of Pride and Prejudice, our Activity did not create it. We are able to read it
because our Activity supports its MIME type. Unfortunately, that MIME type,
application/zip, is used by other Activities. I found it very frustrating to want to open a
book in Read Etexts and accidentally have it opened in EToys instead. This line of
code solves that problem. You only need to use Start Using... the first time you read a
book. After that the book will use the Read Etexts icon and can be resumed with a
single click.
This does not at all affect the MIME type of the Journal entry, so if you wanted to
deliberately open Pride and Prejudice with Etoys it is still possible.
Before we leave the subject of Journal metadata let's look at all the standard metadata
that every Activity has. Here is some code that creates a new Journal entry and updates
a bunch of standard properties:
def create_j ournal_entry (self , tempfile):
j ournal_entry = datastore . create ( )
j ournal_title = self . selected_title
if self . selectedvolurae != ' ' :
j ournal_title += ' ' + _( 'Volume') + ' ' + \
self . selected_volume
if self . selected_author != ' ' :
j ournal_title = j ournal_title + ' , by ' + \
self . selected_author
j ournal_entry . metadata [' title ' ] = j ournal_title
j ournal_entry. metadata [' title_set_by_user ' ] = '1'
j ournal_entry .metadata [' keep ' ] = '0'
format = \
self . _books_toolbar . f ormat_combo .props .value
if format == ' .djvu' :
j ournal_entry .metadata [' mime_type ' ] = \
' image/vnd. djvu '
if format == ' .pdf ' or format == '_bw.pdf' :
j ournal_entry . metadata [' mime_type ' ] = \
59
' application/pdf '
journal_entry .metadata [ 'buddies ' ] = ' '
j ournal_entry .metadata [' preview ' ] = ''
journal_entry .metadata [' icon-color ' ] = \
prof ile . getcolor ( ) .to_string()
textbuffer = self . textview . get_buf f er ( )
j ournal_entry .metadata [' description ' ] = \
textbuffer. get_text (textbuffer . get_start_iter ( ) ,
textbuf f er . get_end_iter ( ) )
j ournal_entry . f ile_path = tempfile
datastore . write ( j ournal_entry)
os . remove (tempfile)
self ._alert (_(' Success ') , self . selected_title + \
_(' added to Journal.'))
This code is taken from an Activity I wrote that downloads books from a website and
creates Journal entries for them. The Journal entries contain a friendly title and a full
description of the book.
Most Activities will only deal with one Journal entry by using the read_file() and
write_file() methods but you are not limited to that. In a later chapter I'll show you how
to create and delete Journal entries, how to list the contents of the Journal, and more.
We've covered a lot of technical information in this chapter and there's more to come,
but before we get to that we need to look at some other important topics:
• Putting your Activity in version control. This will enable you to share your code
with the world and get other people to help work on it.
• Getting your Activity translated into other languages.
• Distributing your finished Activity. (Or your not quite finished but still useful
Activity).
60
-L -L • Add Your Activity Code To Version
Control
What Is Version Control?
"If I have seen further it is only by standing on the shoulders of giants."
Isaac Newton, in a letter to Robert Hooke.
Writing an Activity is usually not something you do by yourself. You will usually have
collaborators in one form or another. When I started writing Read Etexts I copied
much of the code from the Read Activity. When I implemented text to speech I
adapted a toolbar from the Speak Activity. When I finally got my copied file sharing
code working the author of Image Viewer thought it was good enough to copy into
that Activity. Another programmer saw the work I did for text to speech and thought
he could do it better. He was right, and his improvements got merged into my own
code. When I wrote Get Internet Archive Books someone else took the user interface
I came up with and made a more powerful and versatile Activity called Get Books.
Like Newton, everyone benefits from the work others have done before.
Even if I wanted to write Activities without help I would still need collaborators to
translate them into other languages.
To make collaboration possible you need to have a place where everyone can post their
code and share it. This is called a code repository. It isn't enough to just share the latest
version of your code. What you really want to do is share every version of your code.
Every time you make a significant change to your code you want to have the new
version and the previous version available. Not only do you want to have every
version of your code available, you want to be able to compare any two versions your
code to see what changed between them. This is what version control software does.
The three most popular version control tools are CVS, Subversion, and Git. Git is the
newest and is the one used by Sugar Labs. While not every Activity has its code in the
Sugar Labs Git repository (other free code repositories exist) there is no good reason not
to do it and significant benefits if you do. If you want to get your Activity translated
into other languages using the Sugar Labs Git repository is a must.
61
Git Along Little Dogies
Git is a distributed version control system. This means that not only are there copies
of every version of your code in a central repository, the same copies exist on every
user's computer. This means you can update your local repository while you are not
connected to the Internet, then connect and share everything at one time.
There are two ways you will interact with your Git repository: through Git commands
and through the website at http : //git . sugarlab s , org/. We'll look at this website first.
Go to http : //git . sugarlab s . or g/ and click on the Projects link in the upper right corner:
Home Projects Search About Register Login
sugarlabs
You will see a list of projects in the repository. They will be listed from newest to
oldest. You'll also see a New Project link but you'll need to create an account to use
that and we aren't ready to do that yet.
New project
karma_English_Alphabet_Puzzle_Solving
A simple English Alphabet lesson
Categories; none
karma_Conozco- Uruguay
A simple lesson for learning the geography of Uruguay
Categories: none
k ar m a_ad d i n g_u p_t o_1 0_s v g
A simple game for learning how to add up to 1 □
Categories: none
supervisor
A privileged service which supervises activity run cycle, exposes startup progress, upgr
infrastructure.
Categories: service
62
If you use the Search link in the upper right corner of the page you'll get a search form.
Use it to search for "read etexts". dick on the link for that project when you find it. You
should see something like this:
readetexts
Read Etexts is an alternative to the regular Read Activity which can read Project Gutenberg pla
stopgap until Read itself can use this format. Plain text files are by far the most popular Gutent
thousands of free books in many languages.
In addition to the normal ebook reader functions this reader adds text to speech with karaoke st
needs speech-dispatcher installed, which is not currently part of the Sugar distribution but even
Activities
^ alsroot deleted repository readetexts/gst-plugins-espeak
SATURDAY MARCH 07
17:58 L alsm °t deleted repository readetexts/bugfix
FRIDAY FEBRUARY \
22:49 jdsimmons added committer pootle to readetexts/mainline
This page lists some of the activity for the project but I don't find it particularly useful.
To get a much better look at your project start by clicking on the repository name on the
right side of the page. In this case the repository is named mainline.
Labels: activities
License: GNU General Public License version 2(GPLv2)
Owner: jdsimmons
Created: 18 Jan 00:38
Repositories
Q mainline
|i jtfeimmons
63
You'll see something like this at the top of the page:
Overview Commits Source Tree Comments (01 Merge requests(O)
"mainline" repository in readetexts
Public clone url: git: //git, sugarlabs.org/readetexts/mainline, git More inFo...
You can clone this repository with the following command:
git clone git://git .sugarlabs . org/readetexts/mainline. git
HTTP clone url: http://git.sugarlabs.org/git/readetexts/rrainline.git More info..
You can clone this repository with the following command:
git clone http://git.sugarlabs.org/git/readetexts/riiainline, git
(nolelhai cloning over HTTP k slighliy slower, bui iseful F yoi/re behind a Firewall)
Activities O
JNDAY JANUARY IB
23:04 L J dsirnmotls committed 7638697a to readetexts/mainline
modified: ReadEtextsActivity.py
This page has some useful information on it. First, have a look at the Public clone url
and the HTTP clone url. You need to click on More info... to see either one. If you
run either of these commands from the console you will get a copy of the git repository
for the project copied to your computer. This copy will include every version of every
piece of code in the project. You would need to modify it a bit before you could share
your changes back to the main repository but everything would be there.
The list under Activities is not that useful, but if you click on the Source Tree link
you'll see something really good:
64
Tree of mainline repository in readetexts
I main ling
G -gi"
.J. ad
J au
□ gu-
Q he
C3loc
J Mi"
Q NE
□ P9'
C2 pa
J Re
□ reE
Q reE
□ rtfconvert.py
Q .gitignore
01 Sop 23:16
modified :
.gitignore modified: MAN!
C3 activity/
22 Nov 20:E2
modified:
ReadEtextsActivity .py moi
Q ausextract.py
30 May 21:52
modified :
ReadEtextsActivity . py moi
□ gutextract.py
30 May 21:52
modified:
ReadEtextsActivity .py moi
Q help.txt
22 Nov 20:E2
modified :
ReadEtextsActivit y . py moi
C2 locale.''
06 Dec 23:39
new file:
locale/kos/LCJ-IESSAGES/o
J MANIFEST
22 Nov 23:31
modified:
MANIFEST modified: tocali
Q NEWS
01 Mar 20:48
Initial import
_J pgconvert.py
29 Nov 22:34
modified:
ReadEtextsActivity .py moi
Ql paf
1 1 Nov 05:E5
Commit from Sugar Labs: Translatioi
J ReadEtextsActivity.py
29 Nov 22:34
modified :
ReadEtextsActivit y . py moi
□ readsidebar.py
25JuM4:48
modified:
ReadEtextsAetivity .py moi
_] readtoolbar.py
06 Dec 23:39
new file:
locale/kos/LC_MESSAGES/o
22 Nov 23:26
modified: ReadEtextsAetivity .py moi
Here is a list of every file in the project, the date it was last updated, and a comment on
what was modified, dick on the link for ReadEtextsActivity.py and you'll see this:
65
Project Overview
Repositories
Overview Commits Source Tree Comments (□) Merge requests(O)
Blob of ReadEtextsActivity . py (raw blob data)
I mainline ! ReadEtextsActivity. py
1
2
3
4
5
6
7
9
IS
11
12
13
14
15
18
17
IS
19
20
21
22
23
24
25
2B
27
28
#! /usr/bin/env python
# Copyright (O 2003, 20Q9 James D, Simmons
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License., or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the inplied warranty of
ft MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
import os
import logging
import tempfile
import time
import zipfile
import pygtk
pygtk. require! ' 2.0' )
import gtk
import string
from sugar, graphics import style
from sugar import profile
This is the latest code in that file in pretty print format. Python keywords are shown in
a different color, there are line numbers, etc. This is a good page for looking at code on
the screen, but it doesn't print well and it's not much good for copying snippets of code
into Eric windows either. For either of those things you'll want to click on raw blob
data at the top of the listing:
#! /usr/bin/env python
# Copyright (C) 20O8, 2009 James D, Simmons
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License; or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful;
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FDR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os
import logging
import tempfile
import time
import zipfile
import pygtk
pygtk, require! '2,0' )
import gtk
import string
from sugar, graphics import style
from sugar import profile
from sugar, activity import activity
from sugar import network
from sugar, datastore import datastore
from suaar, graphics, alert import NotifvAlert
We're not done yet. Use the Back button to get back to the pretty print listing and click
on the Commits link. This will give us a list of everything that changed each time we
committed code into Git:
67
Overview Commits Source Tree Comments (0) Merge requests(O)
Commitlog for mainline:master in readetexts
SUNDAY DECEMBER 06
23:39 L James Simmons committed cc81 203
new file: locale/ kos/LC_MESSAGES/org .laptop . sugar. ReadEtextsActivity. mo
SUNDAY NOVEMBER
James Simmons committed 720affc
modified: ReadEtextsActivity.py
23 : 3i James Simmons committed 35bf41 7
modified: MANIFEST
L James Simmons committed dc6322a
modified: ReadEtextsActivity.py
~~ James Simmons committed f9cd855
modified: ReadEtextsActivity.py
You may have noticed the odd combination of letters and numbers after the words
James Simmons committed. This is a kind of version number. The usual practice
with version control systems is to give each version of code you check in a version
number, usually a simple sequence number. Git is distributed, with many separate
copies of the repository being modified independently and then merged. That makes
using just a sequential number to identify versions unworkable. Instead, Git gives each
version a really, really large random number. The number is expressed in base 16,
which uses the symbols 0-9 and a-f. What you see in green is only a small part of the
complete number. The number is a link, and if you click on it you'll see this:
68
Overview Commits Source Tree Comments (0) Merge request s(0)
Commit cc81 2030cbf3ec8a51 4275fb97c2ca425b21 6a2f
Date: Sun Dec 06 23:39:56+0000 2009
Committer: James Simmons (jim@simmons.olpc)
Author: James Simmons (jim@simmons.olpc)
Commit SHA1: ccS1 2030cbf3ec8a514275fb97c2ca425b21 6a2f
Tree SHA1: aa0f8aadc7636b9e75047a85c534bf234c993365
new file: locale/kos/LC MESSAGES/org . laptop. sugar . ReadEtextsActivity. mo
new Tile: locale/kos/acTivity.linfo
new file: locale/tzo/LC HESSAGES/org . laptop. sugar . ReadEtextsActivity. mo
new file: locale/tzo/acTivity.linfo
modified: readtoolbar.py
Modify speech toolbar to save and restore speech settings.
Commit diff
Comments (0)
readtoolbar, py 29 --+++++++++++++++++++++++++++
locale/tzo/actLvity.linfo 2 ++
locale/tzo/LC_MESSAGES/org . laptop. sugar. ReadEtextsActivity.mo B
locale/kos/activity.linfo 2 ++
locale/ kos/LC_MESSAGES/org , laptop. sugar, ReadEtextsActivity.mo Q
Commit diff
locale/ko&''LC_MESSAGES/org. laptop, sugar. ReadEtextsActivity.mo
At the top of the page we see the complete version number used for this commit.
Below the gray box we see the full comment that was used to commit the changes.
Below that is a listing of what files were changed. If we look further down the page we
see this:
69
readtoolbar.py
is is # along with this program; it not, write to ths Fres Software
IS 16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, HA 3211Q-L3E1
17 17
is import os
13 19 import logging
19 20 from gettext import gettext as _
20 21 import re
416 416
417 417
418 418
419
420 419
421 1 420
422 421
427 427
428 428
429 429
430
431 430
432 431
433 432
combotool.show()
self .pitchadj = gtk. Adjustment [0, -IBS, IBB, 1, 10, B)
self . pitchad j , connect ( " value_changed" , self . pitch_ad] usted_cb)
pitchbar = gtk. HScale(self .pitchadj)
pitchbar . set_draw_value(False)
pitchbar. set jjpdatejoUcylgtk. UPDATE JHSCONTINUOUS)
_
h
pitchbar, show()
self. rateadj = gtk. Adjustment (B, -IBB, IBB, 1, IB, B)
self. rateadj , connect ("value_changed", self, rate_adjusted_cb)
ratebar = gtk,HScale(self, rateadj)
ratebar , set_draw_value( False)
ratebar.set_update_policy(gtk.UPDATE_DISCON"nNUOUS)
453 453 def pitch_adjusted_cb(self , get):
454 454 speech, pitch = int( get. value)
455 455 speech, say(_("pitch adjusted"))
456 f = open (os, path, join (self , activity,get_activity_root(), 'insta
This is a diff report which shows the lines that have changed between this version and
the previous version. For each change it shows a few lines before and after the change
to give you a better idea of what the change does. Every change shows line numbers
too.
A report like this is a wonderful aid to programming. Sometimes when you're working
on an enhancement to your program something that had been working mysteriously
stops working. When that happens you will wonder just what you changed that could
have caused the problem. A diff report can help you find the source of the problem.
By now you must be convinced that you want your project code in Git. Before we can
do that we need to create an account on this website. That is no more difficult than
creating an account on any other website, but it will need an important piece of
information from us that we don't have yet. Getting that information is our next task.
70
Setting Up SSH Keys
To send your code to the Gitorious code repository you need an SSH public/private
key pair. ! SSH is a way of sending data over the network in encrypted format. (In
other words, it uses a secret code so nobody but the person getting the data can read it).
Public/private key encryption is a way of encrypting data that provides a way to
guarantee that the person who is sending you the data is who he claims to be.
In simple terms it works like this: the SSH software generates two very large numbers
that are used to encode and decode the data going over the network. The first number,
called the private key, is kept secret and is only used by you to encode the data. The
second number, called the public key, is given to anyone who needs to decode your
data. He can decode it using the public key; there is no need for him to know the
private key. He can also use the public key to encode a message to send back to you
and you can decode it using your private key.
Git uses SSH like an electronic signature to verify that code changes that are supposed
to be coming from you actually are coming from you. The Git repository is given your
public key. It knows that anything it decodes with that key must have been sent by
you because only you have the private key needed to encode it.
We will be using a tool called OpenSSH to generate the public and private keys. This
is included with every version of Linux so you just need to verify that it has been
installed. Then use the ssh-keygen utility that comes with OpenSSH to generate the
keys:
[jim@olpc2 ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key ( /home/ j im/ . ssh/id_rsa) :
By default ssh-keygen generates an RSA key, which is the kind we want. By default it
puts the keyfiles in a directory called lyourhomel. ssh and we want that too, so DO NOT
enter a filename when it asks you to. Just hit the Enter key to continue.
[jim@olpc2 ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key ( /home/ j im/ . ssh/id_rsa) :
Created directory ' /home/ j im/ . ssh ' .
Enter passphrase (empty for no passphrase) :
Now we DO want a passphrase here. A passphrase is like a password that is used with
the public and private keys to do the encrypting. When you type it in you will not be
able to see what you typed. Because of that it will ask you to type the same thing again,
and it will check to see that you typed them in the same way both times.
71
[jim@olpc2 ~]$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/h
Created direetorv ' /home /i ira/ . ssh ' .
Created directory ' /home/ j im/ . ssh
Enter passphrase (empty for no passphrase
home/ j im/ . ssh/id_rsa) :
has been saved in /home/jim/
85:c7 : 4c: 9e
Enter same passphrase again:
Your identification has been saved
Your public key has be"' !
The key fingerprint is .
d0:fe:c0:0c:le:72:56:7a:19:cd:f3:
j im@olpc2 . simmons
The key's randomart image is:
^-- r RSA 2048] +
oo E=.
+ o+ .+=.
. B + o . oo
=
in /home/ j im/ . ssh/id_rsa .
1 ^--i ss h/id_rsa . pub .
1!
+ --
When choosing a passphrase remember that it needs to be something you can type
reliably without seeing it and it would be better if it was not a word you can find in the
dictionary, because those are easily broken. When I need to make a password I use the
tool at http: //www. multicians , org/thv v/gpw. html. This tool generates a bunch of
nonsense words that are pronounceable. Pick one that appeals to you and use that.
Now have a look inside the .ssh directory. By convention every file or directory name
that begins with a period is considered hidden by Linux, so it won't show up in a
GNOME file browser window unless you use the option on the View menu to Show
Hidden Files. When you display the contents of that directory you'll see two files:
id_rsa and id_rsa.pub. The public key is in id_rsa.pub. Try opening that file with
gedit (Open With Text Editor) and you'll see something like this:
Id rsa. pub [~/.ssri) ■ gedit
Rle Edit view search Tools Documents Help
New Open Save
Hurt. .
Undo Redo I '_ut Lop
% & m.
Paste Find Replace
|_J id_rsa.pub
SSH- rsa AWWE3NzaClyt3EAAfl*EIWUU>gOveR23a_//l^yl.3TXEZDrCSXR2VpW6HCeMXQqCflVtfiElU-tNra«Fr6
-tc ZKWCtAjyU rfei vn7 kZJY k/fje/ f 2 j spwBOyYf
tTisb KJVWoy kPBFP dHp Jet5 j I* U j 41 Cy KVpc CeYoAlxsp j xMhWOoeh 3Yd PDWn USe F50w3nO qoSn oM3u0u rvJAtM*2 PC5XPc ARX
*d BblSWTLK H6Bh pv3d TVDnPNOKhC Ii9tvSpiSp Wild KpMf / 5HsCJ KKzC 2yh BiGLTnV5«iw'T2 c Jqc 5641 T/v3KF-t«7SsX5U 31B6Wy+»iirg/
IE 3Ys DBQc PZ1 DWa993J rwrC rXRellb Yn5pKJ4DkCD=r= j lmgnip c2 . slmrnns
72
When you create your account on git, sugarlab s. org there will be a place where you can
add your public SSH key. To do that use Select All from the Edit menu in gedit, then
Copy and Paste into the field provided on the web form.
Create A New Project
I'm going to create a new Project in Git for the examples for this book. I need to log in
with my new account and click the New Project link we saw earlier. I get this form,
which I have started filling in:
Create a new project
Title
Make Your Own Sugar Activities Examples
Slug (Tor urls etc)
myo-sugar-activities-examples
Categories (space separated)
activities
License
GNU General PuPlic License vi^J
The Title is used on the website, the Slug is a shortened version of the title without
spaces used to name the Git repository. Categories are optional. License is GPL v2 for
my projects. You can choose from any of the licenses in the list for your own Projects,
and you can change the license entry later if you want to. You will also need to enter a
Description for your project.
Once you have this set up you'll be able to click on the mainline entry for the Project
(like we did with Read Etexts before) and see something like this:
73
Repositories
Overview Commits ScuiceTree Comments (0) Merge requests (0)
"mainline" repository in Make Your Own Sugar Activities Book Examples
Public clone url: git; //git. strgarlabs.org/myo-sugar-aetivities-exariplevrrain line, git nuts Ms. ..
You cart do re th i s repo sito ry with ihe bl lowing command ;
git clone git://git.sLtgarlabs.org/»yD-5ugar-activities-e>raiiples/iainline.git
HTTP clone url: tittp://git,sugarlabs, Drg/git/myo-sugar-activities-exanples/inainline.git r.bro infa..
You can do ne th i s repo sito ry with ihe bl low ing command :
git clone nttf>: //git, sua.ariabi.org/glt/riyo- sugar -activities- exanpies/riamilne. git
(note thai coring ever HTTP Bs*grril/ &kwer. bui Lsetuli yoi/^behoiafre^l)
Puih url! gitoriousggit.sugarlabs. orgimyo-sugar-actiuities-exanples/irainline. gitftfai. irfo..]
You can run -git push gitorious^glt .sugarlabs.org myo-sugar- activities -examples/mainline, git", or you es
tit renote add origin gitorious&jit. sugarlabs.orgiriyO'Suqar-activitie'S-eKaiiples/riainline.git
to push trie laster branch to the origin remite we added above:
git push origin master
* after that yDU can. just do;
git push
Activities E3
The next step is to convert our project files into a local Git repository, add the files to it,
then push it to the repository on git. sugarlab s. org. We need to do this because you
cannot clone an empty repository, and our remote repository is currently empty. To get
around that problem we'll push the local repository out to the new remote repository we
just created, then clone the remote one and delete our existing project and its Git
repository. From then on we'll do all our work in the cloned repository.
This process may remind you of the Edward Albee quote, "Sometimes a person has to
go a very long distance out of his way to come back a short distance correctly".
Fortunately we only need to do it once per project. Enter the commands shown below
in bold after making you project directory the current one:
git init
Initialized empty Git repository in
/home/ j im/olpc/bookexamples/ .git/
git add *.py
git add activity
git add MANIFEST
git add .gitignore
git commit -a -m "Create repository and load"
[master (root-commit) 727bfe8] Create repository and load
9 files changed, 922 insertions ( + ) , deletions (-)
create mode 100644 .gitignore
create mode 100644 MANIFEST
create mode 100755 ReadEtexts .py
create mode 100644 ReadEtextsActivity . py
create mode 100644 ReadEtextsActivity2 . py
create mode 100644 activity/activity . info
74
create mode 100644 activity/read-etexts . svg
create mode 100755 setup. py
create mode 100644 toolbar. py
I have made an empty local Git repository with git init, then I've used git add to add
the important files to it. (In fact git add doesn't actually add anything itself; it just tells
Git to add the file on the next git commit). Finally git commit with the options shown
will actually put the latest version of these files in my new local repository.
To push this local repository to git, sugarlab s. org we use the commands from the web
page:
git remote add origin \
gitorious@git . sugarlabs . org : \
myo-sugar-activi ties-examples/mainline . git
git push origin master
Counting objects: 17, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 7.51 KiB, done.
Total 15 (delta 3), reused (delta 0)
To gitorious@git. sugarlabs. org : myo-sugar-activi ties -examples/
mainline . git
2cb3ale .. 70078 9d master -> master
=> Syncing Gitorious...
Heads up: head of changed to
700789d3333a7257999d0a69bdcafb840e6adc09 on master
Notify cia.vc of 727bf e81 9d5b7b70f 4f 2b31d02f 5562709284ac4 on
myo-sugar-activi ties -examples
Notify cia.vc of 70078 9d3333a7257 999d0a69bdcafb84 0e6adc09 on
myo-sugar-activi ties -examples
[OK]
rm *
rm activity -rf
rm .git -rf
cd ~
rm Activity/ReadEtextsII
mkdir olpc
cd olpc
mkdir bookexamples
cd bookexamples
git clone \
git: //git . sugarlabs . org/\
myo-sugar-activi ties-examples/mainline . git
Initialized empty Git repository in
/home/ j im/ olpc /bookexamples /mainline/ .git/
remote: Counting objects: 18, done,
remote: Compressing objects: 100% (16/16), done,
remote: Total 18 (delta 3), reused (delta 0)
Receiving objects: 100% (18/18), 8.53 KiB, done.
Resolving deltas: 100% (3/3), done.
75
The lines in bold are the commands to enter, and everything else is messages that Git
sends to the console. I've split some of the longer Git commands with the backslash (\ )
to make them fit better on the printed page, and wrapped some output lines that would
normally print on one line for the same reason. It probably isn't clear what we're doing
here and why, so let's take it step by step:
• The first command git remote add origin tells the remote Git repository that we
are going to send it stuff from our local repository.
• The second command git push origin master actually sends your local Git
repository to the remote one and its contents will be copied in. When you enter
this command you will be asked to enter the SSH pass phrase you created in the
last section. GNOME will remember this phrase for you and enter it for every Git
command afterwards so you don't need to. It will keep doing this until you log out
or turn off the computer.
• The next step is to delete our existing files and our local Git repository (which is
contained in the hidden directory .git). The rm .git -rf means "Delete the directory
.git and everything in it", rm is a Unix command, not part of Git. If you like you
can delete your existing files after you create the cloned repository in the next step.
Note the command rm Activity/ReadEtextsII, which deletes the symbolic link to
our old project that we created by running ./setup.py dev. We'll need to go to our
new cloned project directory and run that again before we can test our Activity
again.
• Now we do the git clone command from the web page. This takes the remote Git
repository we just added our MANIFEST file to and makes a new local repository
in directory A/oMr/iome/olpc/bookexamples/mainline.
Finally we have a local repository we can use. Well, not quite. We can commit our code
to it but we cannot push anything back to the remote repository because our local
repository isn't configured correctly yet.
What we need to do is edit the file config in directory .git in
/i/OMr/zome/olpc/bookexamples/mainline. We can use gedit to do that. We need to
change the url= entry to point to the Push url shown on the mainline web page. When
we're done our config file should look like this:
[core]
repositoryf ormatversion =
filemode = true
bare = false
logallref updates = true
[remote "origin"]
url = gitorious@git.sugarlabs.org:
myo-sugar-activi ties-examples/mainline . git
fetch = +ref s/heads/* : ref s/remotes/origin/*
[branch "master"]
76
remote = origin
merge = ref s/heads/master
The line in bold is the only one that gets changed. It is split here to make it fit on the
printed page. In your own files it should all be one line with no spaces between the
colon(:) that ends the first line and the beginning of the second line.
From now on anyone who wants to work on our project can get a local copy of the Git
repository by doing this from within the directory where he wants the repository to go:
git clone git : //git . sugarlabs . org/\
myo-sugar-activi ties-examples/mainline . git
He'll have to change his .git/config file just like we did, then he'll be ready to go.
Everyday Use Of Git
While getting the repositories set up to begin with is a chore, daily use is not. There are
only a few commands you'll need to work with. When we left off we had a repository
in /j/owr/jowe/olpc/bookexamples/mainline with our files in it. We will need to add
any new files we create too.
We use the git add command to tell Git that we want to use Git to store a particular file.
This doesn't actually store anything, it just tells Git our intentions. The format of the
command is simply:
git add file_or_directory_name
There are files we don 't want to add to Git, to begin with those files that end in .pyc. If
we never do a git add on them they'll never get added, but Git will constantly ask us
why we aren't adding them. Fortunately there is a way to tell Git that we really, really
don't want to add those files. We need to create a file named .gitignore using gedit and
put in entries like this:
* .pyc
* .e4p
* . zip
. eric4pro j ect/
. ropepro j ect/
These entries will also ignore project files used by Eric and zip files containing ebooks,
Once we have this file created in the mainline directory we can add it to the repository:
git add .gitignore
git commit -a -m "Add .gitignore file"
77
From now on Git will no longer ask us to add .pyc or other unwanted files that match
our patterns. If there are other files we don't want in the repository we can add them to
.gitignore either as full file names or directory names or as patterns like *.pyc.
In addition to adding files to Git we can remove them too:
git rm filename
Note that this just tells Git that from now on it will not be keeping track of a given
filename, and that will take effect at the next commit. Old versions of the file are still in
the repository.
If you want to see what changes will be applied at the next commit run this:
git status
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will
# be committed)
#
# modified: ReadEtextsActivity .py
#
no changes added to commit (use "git add" and/or
"git commit -a")
Finally to put your latest changes in the repository use this:
git commit -a -m "Change use of instance directory to tmp"
Created commit a687b27: Change use of instance
directory to tmp
1 files changed, 2 insertions ( + ) , 2 deletions (-)
If you leave off the -m an editor will open up and you can type in a comment, then save
and exit. Unfortunately by default the editor is vi, an old text mode editor that is not
friendly like gedit.
When we have all our changes done we can send them to the central repository using
git push:
git push
Counting objects: 5, done.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 322 bytes, done.
Total 3 (delta 2), reused (delta 0)
To gitorious@git.sugarlabs.org:
myo- sugar-activities -examples /mainline .git
700789d. .a687b27 master -> master
=> Syncing Gitorious . . .
Heads up: head of changed to
a687b27e2f 034e5al7d2ca2fe9f2787c7f 633e64 on master
Notify cia.vc of a687b27e2f 034e5al7d2ca2f e9f 27 87c7f 633e64
on myo-sugar-activities-examples
78
[OK]
We can get the latest changes from other developers by doing git pull:
git pull
remote: Counting objects: 17, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 3), reused (delta 0)
Unpacking objects: 100% (15/15), done.
From gitoriousSgit. sugarlabs .org:
myo- sugar- activities -examples /mainline
2cb3ale .. 70078 9d master -> origin/master
Updating 2cb3ale .. 70078 9d
Fast forward
. gitignore
MANIFEST
Re a
Rea
Rea
+ + + +
act
act
set
too
9 f
ere
ere
ere
ere
ere
ere
ere
ere
dEtext
dEtext
dEtext
++++++
ivity/
ivity/
up.py
lbar . p
iles c
ate mo
ate mo
ate mo
ate mo
ate mo
ate mo
ate mo
ate mo
s.py
sActiv
sActiv
++++++
activi
read-e
Y
hanged,
de 100*
de 100
de 100
de 100
de 100
de 100
de 100
de 100 6
6 +
244 +■
182 +++++++++++++++++++++++++++
182 +++++++++++++++++++++++++++
311 +++++++++++++++++++++++++++
+ +
+++++++++++
+ + +
++++++++++++++++++++
+ ) , 241 deletions (-)
ty.py
ty2.py
+ +
y . info | 9
exts . svg | 71
21
136
921 insertions
44 .gitignore
55 ReadEtexts .py
44 ReadEtextsActivity .py
44 ReadEtextsActivity2 .py
44 activity/activity . info
44 activity/read-etexts . svg
55 setup. py
44 toolbar. py
79
J^Ztm Going International With Pootle
Introduction
The goal of Sugar Labs and One Laptop Per Child is to educate all the children of the
world, and we can't do that with Activities that are only available in one language. It is
equally true that making separate versions of each Activity for every language is not
going to work, and expecting Activity developers to be fluent in many languages is not
realistic either. We need a way for Activity developers to be able to concentrate on
creating Activities and for those who can translate to just do that. Fortunately, this is
possible and the way it's done is by using gettext.
Getting Text With gettext
You should remember that our latest code example made use of an odd import:
from gettext import gettext as _
The "_()" function was used in statements like this:
self. back. set_tooltip (_ ( ' Back ' ) )
At the time I explained that this odd looking function was used to translate the word
"B ack" into other languages, so that when someone looks at the B ack button's tool tip
he'll see the text in his own language. I also said that if it was not possible to translate
this text the user would see the word "B ack" untranslated. In this chapter we'll learn
more about how this works and what we have to do to support the volunteers who
translate these text strings into other languages.
The first thing you need to learn is how to properly format the text strings to be
translated. This is an issue when the text strings are actual sentences containing
information. For example, you might write such a message this way:
message = _("User ") + username + \
_(" has joined the chat room.")
This would work, but you've made things difficult for the translator. He has two
separate strings to translate and no clue that they belong together. It is much better to
do this:
message = _("User %s has joined the chat room.") % \
username
80
If you know both statements give the same resulting string then you can easily see why
a translator would prefer the second one. Use this technique whenever you need a
message that has some information inserted into it. When you use it, try and limit
yourself to only one format code (the %s) per string. If you use more than one it can
cause problems for the translator.
Going To Pot
Assuming that every string of text a user might be shown by our Activity is passed
through "_()" the next step is to generate a pot file. You can do this by running setup.py
with a special option:
./setup.py genpot
This creates a directory called po and puts a file ActivityName.pot in that directory. In
the case of our example project ActivityName is ReadEtextsII. This is the contents of
that file:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the
# PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-01-06 18 : 31-0 600\n"
"PO-Revision-Date: YEAR-MO-DA HO :MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li . org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: activity/activity . info : 2
msgid "Read ETexts II"
msgstr ""
# : toolbar . py : 34
msgid "Back"
msgstr ""
#: toolbar .py: 40
msgid "Forward"
msgstr ""
#: toolbar . py: 115
msgid "Zoom out"
msgstr ""
81
#: toolbar .py: 120
msgid "Zoom in"
msgstr ""
#: toolbar . py : 130
msgid "Fullscreen"
msgstr ""
#: ReadEtextsActivity2 .py : 34
msgid "Edit"
msgstr ""
#: ReadEtextsActivity2 .py : 38
msgid "Read"
msgstr ""
#: ReadEtextsActivity2 .py : 46
msgid "View"
msgstr ""
This file contains an entry for every text string in our Activity (as msgid) and a place to
put a translation of that string (msgstr). Copies of this file will be made by the Pootle
server for every language desired, and the msgstr entries will be filled in by volunteer
translators.
Going To Pootle
Before any of that can happen we need to get our POT file into Pootle. The first thing
we need to do is get the new directory into our Git repository and push it out to
Gitorious. You should be familiar with the needed commands by now:
git add po
git commit -a -m "Add POT file"
git push
Next we need to give the user "pootle" commit authority to our Git project. Go to
git. sugarlab s. org, sign in, and find your Project page and click on the mainline link.
You should see this on the page that takes you to:
82
Project: Make Your Own Sugar Activities Book
Examples
Maintained jdsimmors
Created: 03 Jan 21:46
Clone repository
Request merge
Add committer
Committers
jdsimmons (owner)
Qick on the Add committer link and type in the name pootle in the form that takes
you to. When you come back to this page pootle will be listed under Committers.
Your next step is to go to web site http: //bugs. sugarlab s. org and register for a user id.
When you get that open up a ticket something like this:
83
sugarlabs
lagged
Create New Ticket
Properties
Summary:
Description:
Type:
Milestone:
Version:
Keywords:
DistributiorVos:
Assign to:
Add project "Make Youi Own Sugar Activities Book Examples" lo Pootle
B / A * B]— f - B
Diet pootle is alr**dy a committer for this project in Git.
taBk t| Priority: | Unspecified by Mainfainar t|
Unspecified by Release Team ▼] Component: 1 localization ▼]
Unspecified -J Severity: | Unspecified^
Cc:
Unspecilied ^\ Bug Status: |New _J
The Component entry localization should be used, along with Type task.
Believe it or not, this is all you need to do to get your Activity set up to be translated.
Pay No Attention To That Man Behind The Curtain
After this you'll need to do a few things to get translations from Pootle into your
Activity.
• When you add text strings (labels, error messages, etc.) to your Activity always use
the _() function with them so they can be translated.
• After adding new strings always run ./setup.py genpot to recreate the POT file.
• After that commit and push your changes to Gitorious.
• Every so often, and especially before releasing a new version, do a git pull. If
there are any localization files added to Gitorious this will bring them to you.
• After getting a bunch of new files run ./setup.py fix manifest to get the new files
included in your MANIFEST file. Afterwards edit the MANIFEST with gedit to
remove any unwanted entries (which will be Eric project files, etc.).
84
Localization with Pootle will create a large number of files in your project, some in the
po directory and others in a new directory called locale. As long as these are listed in
the MANIFEST they will be included in the .xo file that you will use to distribute your
Activity.
C'est Magnifique!
Here is a screen shot of the French language version of Read Etexts reading Jules
Verne's novel Le tour du monde en quatre-vingts jours:
O ©•H it u
<><><><><><><><><><><><><><><><><><><><><><><><><><!
LETOUADU MONDE
EN
QUATRE-VINGTS JOURS
par Jules Verne
I
DANS LEQUEL PHI LEAS FOGG ET PASSEPAFTOUT
S'ACCEPTENT RECIPROQUEMENT L'UN COM ME HAITRE,
L'AUTRE COM ME D0ME5TIQUE
En I'annee 1B72, la maison portant le numero 7 de Saville-row, Burlington Gardens
-- maison dans laquelle Bheridan mourut en 1S14 — , etait habitee par Phileas Fogg,
esq.. Tun des membres les plus singuliers et les plus remarques du Reform-Club de
Londres, bien qu'il semblat prendre a fcache de re rien faire qui put attirer
I'attention.
There is reason to believe that the book is in French too.
85
A 3 • Distribute Your Activity
Choose A License
Before you give your Activity to anyone you need to choose a license that it will be
distributed under. Buying software is like buying a book. There are certain rights you
have with a book and others you don't have. If you buy a copy of The DaVinci Code
you have the right to read it, to loan it out, to sell it to a used bookstore, or to burn it.
You do not have the right to make copies of it or to make a movie out of it. Software is
the same way, but often worse. Those long license agreements we routinely accept by
clicking a button might not allow you to sell the software when you're done with it, or
even give it away. If you sell your computer you may find that the software you
bought is only good for that computer, and only while you are the owner of the
computer. (You can get good deals on reconditioned computers with no operating
system installed for that very reason).
If you are in the business of selling software you might have to hire a lawyer to draw up
a license agreement, but if you're giving away software there are several standard
licenses you can choose from for free. The most popular by far is called the General
Public License, or GPL. Like the licenses Microsoft uses it allows the people who get
your program to do some things with it but not others. What makes it interesting is not
what it allows them to do (which is pretty much anything they like) but what it forbids
them to do.
If someone distributes a program licensed under the GPL they are also required to
make the source code of the program available to anyone who wants it. That person
may do as he likes with the code, with one important restriction: if he distributes a
program based on that code he must also license that code using the GPL. This makes it
impossible for someone to take a GPL licensed work, improve it, and sell it to someone
without giving him the source code to the new version.
While the GPL is not the only license available for Activities to be distributed on
http : //activities . sugarlab s . org all the licenses require that anyone getting the Activity
also gets the complete source code for it. You've already taken care of that requirement
by putting your source code in Gitorious. If you used any code from an existing
Activity licensed with the GPL you must license your own code the same way. If you
used a significant amount of code from this book (which is also GPL licensed) you may
be required to use the GPL too.
86
Is licensing something you should worry about? Not really. The only reason you'd
want to use a license other than the GPL is if you wanted to sell your Activity instead of
give it away. Consider what you'd have to do to make that possible:
• You'd have to use some language other than Python so you could give someone
the program without giving them the source code.
• You would have to have your own source code repository not available to the
general public and make arrangements to have the data backed up regularly.
• You would have to have your own website to distribute the Activity. The website
would have to be set up to accept payments somehow.
• You would have to advertise this website somehow or nobody would know your
Activity existed.
• You would have to have a lawyer draw up a license for your Activity.
• You would have to come up with some mechanism to keep your customers from
giving away copies of your Activity.
• You would have to create an Activity so astoundingly clever that nobody else could
make something similar and give it away.
• You would have to deal with the fact that your "customers" would be children with
no money or credit cards.
In summary, activities . sugarlab s . org is not the iPhone App Store. It is a place where
programmers share and build upon each other's work and give the results to children
for free. The GPL encourages that to happen, and I recommend that you choose that
for your license.
Add License Comments To Your Python Code
At the top of each Python source file in your project (except setup.py, which is already
commented) put comments like this:
# filename Program description
#
# Copyright (C) 2010 Your Name Here
#
# This program is free software; you can redistribute it
# and/or modify it under the terms of the GNU General
# Public License as published by the Free Software
# Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will
# be useful, but WITHOUT ANY WARRANTY; without even
# the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public
87
# License for more details .
#
# You should have received a copy of the GNU General
# Public License along with this program; if not, write
# to the Free Software Foundation, Inc., 51 Franklin
# St, Fifth Floor, Boston, MA 02110-1301 USA
If the code is based on someone else's code you should mention that as a courtesy.
Create An .xo File
Make certain that activity.info has the version number you want to give your Activity
(currently it must be a positive integer) and run this command:
. /setup. py dist_xo
This will create a dist directory if one does not exist and put a file named something like
ReadETextsII-l.xo in it. The "1" indicates version 1 of the Activity.
If you did everything right this .xo file should be ready to distribute. You can copy it to
a thumb drive and install it on an XO laptop or onto another thumb drive running
Sugar on a Stick. You probably should do that before distributing it any further. I like
to live with new versions of my Activities for a week or so before putting them on
activities . sugarlab s , org.
Now would be a good time to add dist to your .gitignore file, then commit it and push
it to Gitorious. You don't want to have copies of your .xo files in Git. Another good
thing to do at this point would be to tag your Git repository with the version number so
you can identify which code goes with which version.
git tag -m "Release 1" vl HEAD
git push --tags
Add Your Activity To ASLO
When you're ready to post the .xo file on ASLO you'll create an account as you did with
the other websites. When you've logged in there you'll see a Tools link in the upper
right corner of the page. Click on that and you'll see a popup menu with an option for
Developer Hub, which you should click on. That will take you to the pages where you
can add new Activities. The first thing it asks for when setting up a new Activity is
what license you will use. After that you should have no problem getting your Activity
set up.
88
You will need to create an Activity icon as a .gif file and create screen shots of your
Activity in action. You can do both of these things with The GIMP (GNU Image
Manipulation Program). For the icon all you need to do is open the .svg file with The
GIMP and Save As a .gif file.
For the screen shots use sugar-emulator to display your Activity in action, then use the
Screenshot option from the Create submenu of the File menu with these options:
Screenshot
Area
® Take a screenshot of a single window
Include window decoration
Take a screenshot of the entire screen
□ Include mouse pointer
Select a region to grab
Delay
seconds
10
At the end of the delay, click in a window to snap it.
.Help
l Cancel
| Snap
This tells GIMP to wait 10 seconds, then take a screenshot of the window you click on
with the mouse. You'll know that the 10 seconds are up because the mouse pointer will
change shape to a plus (+) sign. You also tell it not to include the window decoration
(which means the window title bar and border). Since windows in Sugar do not have
decorations eliminating the decorations used by sugar-emulator will give you a
screenshot that looks exactly like a Sugar Activity in action.
Every Activity needs one screenshot, but you can have more if you like. Screenshots
help sell the Activity and instruct those who will use it on what the Activity can do.
Unfortunately, ASLO cannot display pictures in a predictable sequence, so it is not
suited to displaying steps to perform.
Another thing you'll need to provide is a home page for your Activity. The one for
Read Etexts is here:
http : //wiki. sugarlab s . org/go/Activities/Read E texts
89
Yes, one more website to get an account for. Once you do you can specify a link with
I gol Activities! some_name and when you click on that link the Wiki will create a page for
you. The software used for the Wiki is MediaWiki, the same as used for Wikipedia.
Your page does not need to be as elaborate as mine is, but you definitely should provide
a link to your source code in Gitorious.
90
A 4. Debugging Sugar Activities
Introduction
No matter how careful you are it is reasonably likely that your Activity will not work
perfectly the first time you try it out. Debugging a Sugar Activity is a bit different than
debugging a standalone program. When you test a standalone program you just run
the program itself. If there are syntax errors in the code you'll see the error messages on
the console right away, and if you're running under the Eric IDE the offending line of
code will be selected in the editor so you can correct it and keep going.
With Sugar it's a bit different. It's the Sugar environment, not Eric, that runs your
program. If there are syntax errors in your code you won't see them right away.
Instead, the blinking Activity icon you see when your Activity starts up will just keep
on blinking for several minutes and then will just go away, and your Activity won't start
up. The only way you'll see the error that caused the problem will be to use the Log
Activity. If your program has no syntax errors but does have logic errors you won't be
able to step through your code with a debugger to find them. Instead, you'll need to
use some kind of logging to trace through what's happening in your code, and again use
the Log Activity to view the trace messages. Now would be a good time to repeat some
advice I gave before:
Make A Standalone Version Of Your Program First
Whatever your Activity does, it's a good bet that 80% of it could be done by a
standalone program which would be much less tedious to debug. If you can think of a
way to make your Activity runnable as either an Activity or a standalone Python
program then by all means do it.
Use PyLint, PyChecker, or PyFlakes
One of the advantages of a compiled language like C over an interpreted language like
Python is that the compiler does a complete syntax check of the code before converting
it to machine language. If there are syntax errors the compiler gives you informative
error messages and stops the compile. There is a utility call lint which C programmers
can use to do even more thorough checks than the compiler would do and find
questionable things going on in the code.
91
Python does not have a compiler but it does have several lint-like utilities you can run
on your code before you test it. These utilities are pyflakes, pychecker, and pylint.
Any Linux distribution should have all three available.
PyFlakes
Here is an example of using PyFlakes:
pyflakes minichat.py
minichat.py:25
minichat.py:28
minichat.py:29
minichat.py:29
'COLOR_BUTTON_GREY' imported but unused
'XoColor' imported but unused
'Palette' imported but unused
' Canvaslnvoker ' imported but unused
PyFlakes seems to do the least checking of the three, but it does find errors like these
above that a human eye would miss.
PyChecker
Here is PyChecker in action:
pychecker ReadEtextsActivity .py
Processing ReadEtextsActivity...
/usr/lib/python2 . 5/site-packages/dbus/_dbus .py : 251 :
DeprecationWarning : The dbus_bindings module is not public
API and will go away soon.
Most uses of dbus_bindings are applications catching
the exception dbus . dbus_bindings . DBusException .
You should use dbus . DBusException instead (this is
compatible with all dbus-python versions since 0.40.2) .
If you need additional public API, please contact
the maintainers via <dbus@lists . f reedesktop . org> .
import dbus . dbus_bindings as m
Warnings . . .
/usr/lib/python2 . 5/site -packages /sugar/act ivity/activity.py: 84 7
Parameter (ps) not used
/usr/lib/python2 . 5/site -package s/sugar/activity/activity.py: 9 92
Parameter (event) not used
/usr/lib/python2 . 5/site -package s/sugar/activity/activity.py: 9 92
Parameter (widget) not used
/usr/lib/python2 . 5/site -package s/sugar/activity/activity.py: 996
Parameter (widget) not used
/usr/lib/python2 . 5/site-packages/sugar/graphics /window. py: 157 :
No class attribute (_alert) found
/usr/lib/python2 . 5/site-packages/sugar/graphics /window. py: 164 :
Parameter (window) not used
/usr/lib/python2 . 5/site-packages/sugar/graphics /window . py : 188 :
Parameter (widget) not used
92
/usr/lib/python2. 5/ site-packages /sugar/graphics /window. py: 2 00:
Parameter (event) not used
/usr/lib/python2. 5/ site-packages /sugar/graphics /window. py: 2 00:
Parameter (widget) not used
ReadEtextsActivity . py : 62 : Parameter (widget) not used
4 errors suppressed, use -#/--limit to increase the number
of errors displayed
PyChecker not only checks your code, it checks the code you import, including Sugar
code.
PyLint
Here is PyLint, the most thorough of the three:
pylint ReadEtextsActivity .py
No config file found, using default configuration
Module ReadEtextsActivity
"*■"*■"*■"*■"*■"*■"*■"*■"*■"*■"*■-*■■*■
c
177 :
c
1 :
c
27 :
Line too long (96/80)
Missing docstring
Operator not preceded by a space
page=0
C: 27 :
( ( [A-Z
C: 30:
C: 174 :
match
W: 30 :
in cla
30:
33:
62:
Too ma
C: 88:
W: 89 :
Using
C: 90:
Operat
Invalid name "page" (should match
_] [A-Z0-9J *) | ( .* ) ) $)
ReadEtextsActivity: Missing docstring
ReadEtextsActivity . read_file : Invalid name "zf" (should
[a-z_] [a-z0-9_] {2,30}$)~
ReadEtextsActivity: Method 'write_file' is abstract
ss 'Activity' but is not overridden
ReadEtextsActivity: Too many ancestors (12/7)
ReadEtextsActivity. init : Using the global statement
ReadEtextsActivity. keypress_cb:
ny return statements (7/6)
ReadEtextsActivity. page_previous :
ReadEtextsActivity. page_previous :
the global statement
ReadEtextsActivity. page_previous :
or not preceded by a space
page=page-l
Missing docstring
C: 91: ReadEtextsActivity. page_previous :
Operator not preceded by a space
if page < 0: page=0
C: 91 : ReadEtextsActivity . page_previous : More than one
statement on a single line
C: 96 : ReadEtextsActivity . page next : Missing docstring
W: 97 : ReadEtextsActivity . page_next : Using the global
statement
C: 98 : ReadEtextsActivity . page_next : Operator not preceded
by a space
page=page+l
93
C: 99 : ReadEtextsActivity .pagenext : More than one
statement on a single line
C : 104 : ReadEtextsActivity . font_decrease : Missing docstring
C : 112 : ReadEtextsActivity . font_increase : Missing docstring
C : 118 : ReadEtextsActivity . scroll_down : Missing docstring
C : 130 : ReadEtextsActivity . scroll_up : Missing docstring
C : 142 : ReadEtextsActivity . show_page : Missing docstring
W : 143 : ReadEtextsActivity . show_page : Using global for
'PAGE_SIZE' but no assigment is done
W : 143 : ReadEtextsActivity . show_page : Using global for
' current_word ' but no assigment is done
W: 157 : ReadEtextsActivity. save_extracted_f ile : Redefining
name 'zipfile' from outer scope (line 21)
C:163: ReadEtextsActivity. save_extracted_f ile : Invalid
name "f" (should match [a-z_] Ta-z0-9_] { 2 , 30 } $ )
W : 171 : ReadEtextsActivity . readf ile : Using global
for 'PAGE_SIZE' but no assigment is done
C : 177 : ReadEtextsActivity . readf ile : Invalid name
"currentFileName" (should match [a-z_] [a-z0-9_] { 2 , 30 } S )
C : 17 9 : ReadEtextsActivity . readf ile : Invalid name
"currentFileName" (should match [a-z_] [a-z0-9_] { 2 , 30 } $ )
C : 197 : ReadEtextsActivity . make_new_f ilename : Missing
docstring
R : 1 97 : ReadEtextsActivity .make_new_f ilename : Method could be
a function
R: 30 : ReadEtextsActivity : Too many public methods (350/20)
W: 174 : ReadEtextsActivity. readf ile : Attribute
'zf' defined outside init
W: 181 : ReadEtextsActivity. readf ile : Attribute
'etext_file' defined outside init
W: 175: ReadEtextsActivity. readf ile : Attribute
'book_files' defined outside init
W: 182 : ReadEtextsActivity. readf ile : Attribute
'page_index' defined outside init
... A bunch of tables appear here . . .
Global evaluation
Your code has been rated at 7.52/10 (previous run: 7.52/10)
PyLint is the toughest on your code and your ego. It not only tells you about syntax
errors, it tells you everything someone might find fault with in your code. This includes
style issues that won't affect how your code runs but will affect how readable it is to
other programmers.
94
The Log Activity
When you start testing your Activities the Log Activity will be like your second home. !
It displays a list of log files in the left pane and when you select one it will display the
contents of the file in the right pane. Every time you run your Activity a new log file is
created for it, so you can compare the log you got this time with what you got on
previous runs. The Edit toolbar is especially useful. It contains a button to show the
log file with lines wrapped (which is not turned on by default but probably should be).
It has another button to copy selections from the log to the clipboard, which will be
handy if you want to show log messages to other developers.
The Tools toolbar has a button to delete log files. I've never found a reason to use it.
Log files go away on their own when you shut down sugar-emulator.
datastore.log
ret. flossmamjals. Minichat-l.log
org.laotop.Log-l.lrjrj
presenceservice.log
sheil.log
telepathy-: alut .log
booUag
booUog-20100122
boot. log- 2 Ql cio 124
fcorjt.log-20100206
tOOt.log-20100215
drnesg
dmesg.cld
lasting
HUbHU pjJKy iBuuuy uujctL dl UXdiULUW I l>Uyai f|.,- ciT ii.. c
buddy+Buddy at fea566c58]>
1266714541.986980 DEBUS root: Activity. write_file is not
implemented.
12667 1454 L 99736 L DEBUG root: data store, write
12667 1454 1. 999744 DEBUS root: dbu sjielpers. update : Ldb7f629-
d202 - 409f - 923e - Qc 1258925027, /h ome/j in/ . su ga r/def au It/
data/ ldb7f 629 - d2Q2- 4Q9f - 923e- oc 1 2QB92Qb27. doc , {dbu s. string
u 'activity _id ' ) : dbus.ByteArray
I '4c222799748a56b24e8e29c7dBc585f6Be9f512c ' ,
vafiant_level=i) , dbus.Stringlu 'title_set_by_user' ) :
dbus.ByteArray ( 'B' , variant_level=l] , dbus.String(u 'uid' ] :
dbu 5 . ByteA rray [ ' idb7f 629 - d2B2 - 4B9f - 923e - Be 12B892Bb27 ' ,
vanant_level=l] , dbus.Stringlu 'title' ) : dbu s . ByteA r ray
{'Mini Chat Activity, variant_level=L), dbu &. String
(u 'timestamp' ): 1266714541, dbu s. St ring (u' activity ) :
dbu s. ByteA rray [ 'net.flossmanuals.MiniChat',
vanant_level=l] , dbu 5, St ring lu 'share- scope' ) :
|dbus. ByteA rray ('public, variant_level=l) , dbus. string
[u'keep'): dbu s . ByteA r ray [ 'fl' , variant_level=l) , dbus. String
fu 'icon-color' J: dbus.ByteArrayl '#BBABFF,#FF2B34' ,
|vanant_level=l] , dbus.Stringlu 'mtime' ] :
■281fl-82-2LTaUa9:8L. 999242*. dbu 6. St ring (u 'preview' ) :
'omitted;-', dbus.string(u'miire_type') : dbu s. ByteA rray
I 'text;' plain ' , variant_level=L] >, True
1266714542,046518 DEBUS root: Written object Idb7f629-
|d2S2-459f-923e-5cl2B392Bb27 to the datastore.
1266714542. 117447 DEBUG root: Activity. _save_cb
Here is what the Log Activity looks like showing a syntax error in your code:
95
T" /tiorne/jim/.sugarAJefault/logs
4a Castors Jog
net , flossrnanu als .Mini ch«
ci»j Jap Lop Analyze -l.log
org.la.pcop-Log-l.log
4Kj.laptop.Log-2.loa.
presenceservice.log
shell.log
telepathy-salut.log
A]sr/lib/python2.6/site-paekages/sugar/util.py:25:
DeprecationWaming: the sha nodule is deprecated; use the hashlib
nodule instead
inport sha
Traceback (most recent call last):
File vusr/bin/sugar- activity, line 21, in -aTodule*
main .main ()
Fa le " /u 5 r/ lib/py thon 2 . 6/ si te - pac kage 5/ su ga r/ac tivi ty/main , py " ,
line 112, in main
module = inport (modulejname)
File Vu sr/share/sugar/activities/Speak. activity/activity, py 1 ',
line 59, in <module>
import voice
File "/usr/sha re/ sugar/activities/speak, activity/voice. py", line
104
as = re. split ( r" ['a-z]+', a. lower!))
v(j, lux. olpc.speak-l.log
v /uac/iog
bootlog
tOOtJog- 20100122
b00t.bg- 20100124
bOTt-log-20100206
bOOt.log-20100215
^^yntaxErrar: invalid syntax
IRl266716031, 992572 DEBUS root;
_c leanu p_tenc_f i le 5
Logging
Without a doubt the oldest debugging technique there is would be the simple print
statement. If you have a running program that misbehaves because of logic errors and
you can't step through the code in a debugger to figure out what's happening you might
print statements in your code. For instance, if you aren't sure that a method is ever
getting executed you might put a statement like this as the first line of the method:
def my_method() :
print 'my_method () begins'
You can include data in your print statements too. Suppose you need to know how
many times a loop is run. You could do this:
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + Unicode (line,
■ iso-8859-1' )
linecount = linecount + 1
print ' linecount=' , linecount
The output of these print statements can be seen in the Log Activity. When you're
finished debugging your program you would remove these statements.
96
An old programming book I read once made the case for leaving the statements in the
finished program. The authors felt that using these statements for debugging and them
removing them is a bit like wearing a parachute when the plane is on the ground and
taking it off when it's airborne. If the program is out in the world and has problems you
might well wish you had those statements in the code so you could help the user and
yourself figure out what's going on. On the other hand, print statements aren't free.
They do take time to run and they fill up the log files with junk. What we need are
print statements that you can turn on an off.
The way you can do this is with Python Standard Logging. In the form used by most
Activities it looks like this:
self._logger = logging . getLogger (
' read-etexts-activity ' )
These statements would go in the init () method of your Activity. Every time you
want to do a print() statement you would do this instead:
def _shared_cb ( self , activity):
self ._logger . debug ( 'My activity was shared')
self . initiating = True
self ._sharing_setup ( )
self ._logger . debug (
'This is my activity: making a tube. . . ')
id = self .tubes_chan [telepathy. CHANNEL_TYPE_TUBES] . \
Of ferDBusTube (SERVICE, {})
def _sharing_setup ( self ) :
if self ._shared_activity is None:
self ._logger . error (
'Failed to share or join activity')
return
Notice that there are two kinds of logging going on here: debug and error. These are
error levels. Every statement has one, and they control which log statements are run
and which are ignored. There are several levels of error logging, from lowest severity to
highest:
self . _logger . debug ( "debug message" )
self. _logger. info ("info message" )
self. _logger. warn ("warn message" )
self. _logger. error ("error message" )
self. _logger. critical ("critical message" )
When you set the error level in your program to one of these values you get messages
with that level and higher. You can set the level in your program code like this:
self. _logger.setLevel( logging. DEBUG)
97
You can also set the logging level outside your program code using an environment
variable. For instance, in Sugar .82 and lower you can start sugar-emulator like this:
SUGAR_LOGGER_LEVEL=debug sugar-emulator
The way you accomplish the same thing in Sugar .84 and greater is to edit the file
~/.sugar/debug and uncomment the line that sets the SUGARLOGGERLEVEL.
The Analyze Activity
Another Activity you may find yourself using at some point is Analyze. This is more
likely to be used to debug Sugar itself than to debug your Activity. If, for instance, your
collaboration test environment doesn't seem to be working this Activity might help you
or someone else figure out why.
I don't have a lot to say about this Activity here, but you should be aware that it exists.
INFO
INFO
INFO
INFO
INFO
INFO
...II
Activity jorg/laptop/sugar;presencejActivitieyi emitted Newchlannel( , ..7Muccbanne^l■) or mentioned the cb^
Activity /org/]aptop/SLigarVPresence./ActivitJe^i emitted JJewCliannei[".../Muc"ILibe£Channell") or mentioned tt
Activity jarg/laptopsugar/Presence/Activitieyi emitted Buddyjoined(".../lseyidf7dbce<}7d43a5b5C48e50f3358s'
< Buddy /org/laptopysugary Presence/Buddi es^ceyidyTdbc e B7 d43 a5b 504Be5 □ f33 5SE5B22?2B99c 9e7 1 >.Get Prope
Bu ddy /Qfg/lap topysugar/Pne&enc e/Buddies/keyi d/7 d b c eo 7d43a 5b5Q4Be 50f335BB5B 2 2 2 2B 9 E c 9e T 7 emitt ed Acti
Bu ddy /org /I ap topjsugar/Pnesenc e/Buddies/keyi dy7-db c eo 7d43a 5b504Be 5Cf335BB5B 2 2 2 2B g 3 c 9e 7 7 emitt ed Tele
Activities;
4c222799748a56b24e8e29c7dOc585f60a9f512c #FF2E34 r #00A0FF net.flossmanjals.MmiChat Mm
Buddies:
Object path
.Jl<eyidv7dbce<)7d43a5b5048e50f33588582222899c9e77 580 bytes, shal 7dbce07d43a5b5u48e50f33538532222399
98
ADVANCED TOPICS
15. Making Shared Activities
16. Adding Text To Speech
17. Fun With The Journal
18. Making Activities Using PyGame
19. Making New Style Toolbars
99
-L 3 • Making Shared Activities
Introduction
One of the distinctive features of Sugar is how many Activities support being used by
more than one person at a time. More and more computers are being used as a
communications medium. The latest computer games don't just pit the player against
the computer; they create a world where players compete against each other. Websites
like Facebook are increasingly popular because they allow people to interact with each
other and even play games. It is only natural that educational software should support
these kinds of interactions.
I have a niece that is an enthusiastic member of the Club Penguin website created by
Disney. When I gave her Sugar on a Stick Blueberry as an extra Christmas gift I
demonstrated the Neighborhood view and told her that Sugar would make her whole
computer like Club Penguin. She thought that was a pretty cool idea. I felt pretty cool
saying it.
Running Sugar As More Than One User
Before you write any piece of software you need to give some thought to how you will
test it. In the case of a shared Activity you might think you'd need more than one
computer available to do testing, but those who designed Sugar did give some thought
to testing shared Activities and gave us ways to test them using only one computer.
These methods have been evolving so there are slight variations in how you test
depending on the version of Sugar you're using. The first thing you have to know is
how to run multiple copies of Sugar as different users.
Fedora 10 (Sugar .82)
In Sugar .82 there is a handy way to run multiple copies of sugar-emulator and have
each copy be a different user, without having to be logged into your Linux box as more
than one user. On the command line for each additional user you want add a
SUGARPROFILE environment variable like this:
SUGAR_PROFILE=austen sugar-emulator
100
When you do this sugar-emulator will create a directory named austen under -/.sugar to
store profile information, etc. You will be prompted to enter a name and select colors
for your icon. Every time you launch using the SUGARPROFILE of austen you will
be this user. If you launch with no SUGARPROFILE you will be the regular user you
set up before.
Fedora 11 (Sugar .84)
As handy as using SUGARPROFILE is the developers of Sugar decided it had
limitations so with version .84 and later it no longer works. With .84 and later you need
to create a second Linux user and run your sugar-emulators as two separate Linux
users. In the GNOME environment there is an option Users and Groups in the
Administration submenu of the System menu which will enable you to set up a
second user. B efore it comes up it will prompt you for the administrative password you
created when you first set up Linux.
Creating the second user is simple enough, but how do you go about being logged in as
two different users at the same time? It's actually pretty simple. You need to open a
terminal window and type this:
ssh -XY jaustenSlocalhost
where "jausten" is the userid of the second user. You will be asked to verify that the
computer at "localhost" should be trusted. Since "localhost" just means that you are
using the network to connect to another account on the same computer it is safe to
answer "yes". Then you will be prompted to enter her password, and from then on
everything you do in that terminal window will be done as her. You can launch sugar-
emulator from that terminal and the first time you do it will prompt you for a name and
icon colors.
sugar-jhbuild
With sugar-jhbuild (the latest version of Sugar) things are a bit different again. You will
use the method of logging in as multiple Linux users like you did in .84, but you won't
get prompted for a name. Instead the name associated with the userid you're running
under will be the name you'll use in Sugar. You won't be able to change it, but you will
be able to choose your icon colors as before.
You will need a separate install of sugar-jhbuild for each user. These additional installs
will go quickly because you installed all the dependencies the first time.
101
Connecting To Other Users
Sugar uses software called Telepathy that implements an instant messaging protocol
called XMPP (Extended Messaging and Presence Protocol). This protocol used to be
called Jabber. In essence Telepathy lets you put an instant messaging client in your
Activity. You can use this to send messages from user to user, execute methods
remotely, and do file transfers.
There are actually two ways that Sugar users can join together in a network:
Salut
If two computer users are connected to the same segment of a network they should be
able to find each other and share Activities. If you have a home network where
everyone uses the same router you can share with others on that network. This is
sometimes called Link-Local XMPP. The Telepathy software that makes this possible is
called Salut.
The XO laptop has special hardware and software to support Mesh Networking, where
XO laptops that are near each other can automatically start networking with each other
without needing a router. As far as Sugar is concerned, it doesn't matter what kind of
network you have. Wired or wireless, Mesh or not, they all work.
Jabber Server
The other way to connect to other users is by going through a Jabber Server. The
advantage of using a Jabber server is you can contact and share Activities with people
outside your own network. These people might even be on the other side of the world.
Jabber allows Activities in different networks to connect when both networks are
protected by firewalls. The part of Telepathy that works with a Jabber server is called
Gabble.
Generally you should use Salut for testing if at all possible. This simplifies testing and
doesn't use up resources on a Jabber server.
It does not matter if your Activity connects to others using Gabble or Salut. In fact, the
Activity has no idea which it is using. Those details are hidden from the Activity by
Telepathy. Any Activity that works with Salut will work with Gabble and vice versa.
To set up sugar-emulator to use Salut go to the Sugar control panel:
102
In Sugar .82 this menu option is Control Panel. In later versions it is My Settings.
103
Qick on the Network icon.
104
Wireless
Turn off the wireless radio to save battery life
Radio
Discard network history if you have
trouble connecting to the network
Discard network history
Mesh
Server:
I
The Server field in this screen should be empty to use Salut. You can use the
backspace key to remove any entry there.
You will need to follow these steps for every Sugar user that will take part in your test.
If for some reason you wish to test your Activity using a Jabber server the OLPC Wiki
maintains a list of publicly available servers at
http://wiki.laptop.org/go/Community Jabber Servers .
Once you have either Salut or a Jabber server set up in both instances of Sugar that you
are running you should look at the Neighborhood view of both to see if they can detect
each other, and perhaps try out the Chat Activity between the two. If you have that
working you're ready to try programming a shared Activity.
The MiniChat Activity
Just as we took the Read Etexts Activity and stripped it down to the basics we're going
to do the same to the Chat Activity to create a new Activity called MiniChat. The real
Chat Activity has a number of features that we don't need to demonstrate shared
Activity messaging:
105
• It has the ability to load its source code into Pippy for viewing. This was a feature
that all Activities on the XO were supposed to have, but Chat is one of the few that
implemented it. Personally, if I want to see an Activity's code I prefer to go to
git.sugarlabs.org where I can see old versions of the code as well as the latest.
• Chat can connect one to one with a conventional XMPP client. This may be useful
for Chat but would not be needed or desirable for most shared Activities.
• If you include a URL in a Chat message the user interface enables you to click on
the URL make a Journal entry for that URL. You can then use the Journal to open
it with the Browse Activity. (This is necessary because activities cannot launch
each other). Pretty cool, but not needed to demonstrate how to make a shared
Activity.
• The chat session is stored in the Journal. When you resume a Chat entry from the
Journal it restores the messages from your previous chat session into the user
interface. We already know how to save things to the Journal and restore things
from the Journal, so MiniChat won't do this.
The resulting code is about half as long as the original. I made a few other changes too:
• The text entry field is above the chat messages, instead of below. This makes it
easier to do partial screenshots of the Activity in action.
• I removed the new style toolbar and added an old style toolbar, so I could test it in
Fedora 10 and 11 which don't support the new toolbars.
• I took the class TextChannelWrapper and put it in its own file. I did this because
the class looked like it might be useful for other projects.
The code and all supporting files for MiniChat are in the MiniChat directory of the Git
repository. You'll need to run
. /setup . py dev
on the project to make it ready to test. The activity.info looks like this:
[Activity]
name = Mini Chat
service_name = net . flossmanuals .MiniChat
icon = chat
exec = sugar-activity minichat .MiniChat
show_launcher = yes
activity version = 1
license = GPLv2+
Here is the code for textchannel.py:
import logging
from telepathy . client import Connection, Channel
from telepathy . interfaces import (
106
CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP,
CHANNEL_TYPE_TEXT, CONN_INTERFACE_ALIASING)
from telepathy . constants import (
CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES,
CHANNEL_TEXT_MESSAGE_TYPE_NORMAL)
class TextChannelWrapper (ob j ect ) :
"""Wrap a telepathy Text Channel to make
usage simpler."""
def init (self, textchan, conn) :
"""Connect to the text channel
self ._activity_cb = None
self ._activity_close_cb = None
self ._text_chan = text_chan
self ._conn = conn
self._logger = logging . getLogger (
'minichat-activity. TextChannelWrapper ' )
self ._signal_matches = []
m = self ._text_chan [CHANNEL_INTERFACE] . \
connect_to_signal (
'Closed', self ._closed_cb)
self . _signal_matches . append (m)
def send(self, text):
"""Send text over the Telepathy text channel."""
# XXX Implement CHANNEL_TEXT_MESSAGE_TYPE_ACTION
if self ._text_chan is not None:
self ._text_chan [CHANNEL_TYPE_TEXT] . Send (
CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text)
def close ( self) :
"""Close the text channel."""
self ._logger . debug (' Closing text channel')
try :
self ._text_chan [CHANNEL_INTERFACE] .Close ()
except :
self . _logger . debug (' Channel disappeared ! ' )
self . _closed_cb ( )
def _closed_cb ( self ) :
"""Clean up text channel.
self ._logger . debug (' Text channel closed.')
for match in self ._signal_matches :
match . remove ( )
self ._signal_matches = []
self ._text_chan = None
if self ._activity_close_cb is not None:
self ._activity_close_cb ( )
def set_received_callback (self , callback):
"""Connect the function callback to the signal.
callback -- callback function taking buddy
and text args
ii ii ii
if self ._text_chan is None:
return
self ._activity_cb = callback
107
m = self ._text_chan [CHANNEL_TYPE_TEXT] . \
connect_to_signal (
'Received', self ._received_cb)
self . _signal_matches . append (m)
def handle_pending_messages (self ) :
"""Get pending messages and show them as
received. " ""
for id, timestamp, sender, type, flags, text \
in self ._text_chan [
CHANNEL_TYPE_TEXT] . ListPendingMessages (
False) :
self ._received_cb (id, timestamp, sender,
type, flags, text)
def _received_cb (self , id, timestamp, sender,
type, flags, text) :
"""Handle received text from the text channel.
Converts sender to a Buddy.
Calls self ._activity_cb which is a callback
to the activity.
ii ii ii
if self ._activity_cb :
buddy = self ._get_buddy (sender )
self ._activity_cb (buddy, text)
self . _text_chan [
CHANNEL_TYPE_TEXT] .
AcknowledgePendingMessages ( [id] )
else :
self . _logger . debug (
'Throwing received message on the floor'
' since there is no callback connected. See
' set_received_callback ' )
def set_closed_callback (self , callback):
"""Connect a callback for when the text channel
is closed.
callback -- callback function taking no args
self ._activity_close_cb = callback
def _get_buddy (self , cs_handle):
"""Get a Buddy from a (possibly channel-specific)
handle. """
# XXX This will be made redundant once Presence
# Service provides buddy resolution
from sugar . presence import presenceservice
# Get the Presence Service
pservice = presenceservice . get_instance ( )
# Get the Telepathy Connection
tp_name, tp_path = \
pservice. get_pref erred_connection ( )
conn = Connection (tpname, tp_path)
group = self ._text_chan [CHANNEL_INTERFACE_GROUP]
my_csh = group . GetSelf Handle ( )
108
if my_csh == cs_handle:
handle = conn . GetSelf Handle ( )
elif group . GetGroupFlags ( ) & \
CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
handle = group . GetHandleOwners ( [cs_handle] ) [0]
else :
handle = cs_handle
# XXX: deal with failure to get the handle owner
assert handle !=
return pservice . get_buddy_by_telepathy_handle (
tp_name, tp_path, handle)
Here is the code for minichat.py:
from gettext import gettext as
import hippo
import gtk
import pango
import logging
from sugar . activity . activity import (Activity,
ActivityToolbox, SCOPE_PRIVATE)
from sugar . graphics . alert import NotifyAlert
from sugar . graphics . style import (Color, COLOR_BLACK,
COLOR_WHITE, COLOR_BUTTON_GREY, FONT_BOLD,
FONT_NORMAL)
from sugar . graphics . roundbox import CanvasRoundBox
from sugar . graphics . xocolor import XoColor
from sugar . graphics . palette import Palette, Canvaslnvoker
from textchannel import TextChannelWrapper
logger = logging . getLogger (' minichat-activity ' )
class MiniChat (Activity) :
def init (self, handle) :
Activity. init (self, handle)
root = self .make_root ( )
self . set_canvas (root)
root . show_all ( )
self . entry . grab_focus ()
toolbox = ActivityToolbox (self )
activity_toolbar = toolbox . get_activity_toolbar ( )
activity_toolbar . keep .props . visible = False
self . set_toolbox (toolbox)
toolbox . show ( )
self. owner = self .pservice . get_owner ( )
# Auto vs manual scrolling:
self ._scroll_auto = True
self ._scroll_value = 0.0
# Track last message, to combine several
# messages:
self ._last_msg = None
self ._last_msg_sender = None
109
self . text_channel = None
if self ._shared_activity :
# we are joining the activity
self.connect('joined', self ._j oined_cb)
if self . get_shared ( ) :
# we have already joined
self . _j oined_cb ( )
else :
# we are creating the activity
if not self .metadata or self . metadata . get (
' share-scope ' ,
SCOPE_PRIVATE) == SCOPE_PRIVATE :
# if we are in private session
self ._alert (_( 'Off-line' ) ,
_( 'Share, or invite someone.'))
self .connect ( 'shared' , self . _shared_cb)
def _shared_cb (self , activity):
logger . debug (' Chat was shared')
self ._setup ( )
def _setup(self) :
self . text_channel = TextChannelWrapper (
self . _shared_activity . telepathy_text_chan,
self . _shared_activity . telepathy_conn)
self . text_channel . set_received_callback (
self . _received_cb)
self._alert(_( 'On-line') , _('Connected') )
self . _shared_activity . connect ( ' buddy- j oined ' ,
self . _buddy_j oined_cb)
self . _shared_activity .connect ( 'buddy-left' ,
self . _buddy_lef t_cb)
self. entry. setsensitive (True)
self . entry . grab_focus ()
def _j oined_cb (self , activity):
"""Joined a shared activity."""
if not self ._shared_activity :
return
logger . debug (' Joined a shared chat')
for buddy in \
self . _shared_activity . get_j oined_buddies ( ) :
self ._buddy_already_exists (buddy)
self ._setup ( )
def _received_cb (self , buddy, text) :
"""Show message that was received."""
if buddy:
nick = buddy .props . nick
else :
nick = ' ??? '
logger . debug (
'Received message from %s: %s', nick, text)
self . add_text (buddy, text)
def _alert(self, title, text=None):
alert = Notif yAlert (timeout=5 )
110
alert . props . title = title
alert . props .msg = text
self . add_alert (alert)
alert . connect ( ' response ' , self . _alert_cancel_cb)
alert . show ( )
def _alert_cancel_cb (self , alert, response_id) :
self . remove_alert (alert)
def _buddy_j oined_cb (self, activity, buddy) :
"""Show a buddy who joined"""
if buddy == self. owner:
return
if buddy:
nick = buddy . props . nick
else :
nick = ' ??? '
self . add_text (buddy, buddy .props . nick+ '
' +_ ( 'joined the chat'),
statu s_message=True)
def _buddy_lef t_cb (self, activity, buddy) :
"""Show a buddy who joined"""
if buddy == self. owner:
return
if buddy:
nick = buddy . props . nick
else :
nick = ' ??? '
self . add_text (buddy, buddy .props . nick+ '
'+_( 'left the chat ' ) ,
statu s_message=True)
def _buddy_already_exists (self , buddy):
"""Show a buddy already in the chat."""
if buddy == self. owner:
return
if buddy:
nick = buddy . props . nick
else :
nick = ' ??? '
self . add_text (buddy, buddy .props . nick+
' '+_( ' is here ' ) ,
statu s_message=True)
def make_root ( self ) :
conversation = hippo . CanvasBox (
spacing=0 ,
background_color=COLOR_WHITE . get_int ( ) )
self . conversation = conversation
entry = gtk.EntryO
entry. modi fyjog (gtk . STATE_INSENSITIVE,
COLOR_WHITE . get_gdk_color ( ) )
entry. modi fyjoase (gtk . STATE_INSENSITIVE,
COLOR_WHITE . get_gdk_color ( ) )
entry. set_sensitive (False)
entry. connect ( 'activate' ,
111
self . entry_activate_cb)
self .entry = entry
hbox = gtk.HBox ()
hbox. add (entry)
sw = hippo . CanvasScrollbars ( )
sw. set_policy (hippo . ORIENTATION_HORIZONTAL,
hippo . SCROLLBAR_NEVER)
sw. set_root (conversation)
self . scrolled_window = sw
vadj = self . scrolled_window .props .widget . \
get_vadjustment ()
vadj .connect ( 'changed' , self .rescroll)
vadj .connect ( 'value-changed' ,
self . scroll_value_changed_cb)
canvas = hippo . Canvas ( )
canvas . set_root (sw)
box = gtk . VBox (homogeneous=False)
box . pack_start (hbox, expand=False)
box . pack_start (canvas)
return box
def rescroll ( self , adj , scroll=None) :
"""Scroll the chat window to the bottom"""
if self ._scroll_auto :
adj . set_value (adj . upper -adj . page_size)
self ._scroll_value = adj . get_value ( )
def scroll_value_changed_cb (self , adj, scroll=None) :
"""Turn auto scrolling on or off.
If the user scrolled up, turn it off.
If the user scrolled to the bottom, turn it back on.
ii ii ii
if adj . getvalue ( ) < self ._scroll_value :
self ._scroll_auto = False
elif adj . get_value ( ) == adj . upper-adj . page_size :
self ._scroll_auto = True
def add_text ( self , buddy, text, status_message=False) :
"""Display text on screen, with name and colors.
buddy -- buddy object
text -- string, what the buddy said
status_message -- boolean
False: show what buddy said
True: show what buddy did
hippo layout:
. rb .
| +name_vbox+ + msg_vbox 1-
II ' II "~ II
| | nick: | | H msg_hbox 1- |
112
text
-l msg hbox-
I text
- +
1
if b
uddy
nick
colo
try:
= buddy . props . nick
r = buddy . props . color
\
# Sel
coloi
coloi
c
coloi
if cc
html =
color :
(
color_stroke_html, color_f ill_html
color . split ( ' , ' )
;pt ValueError:
color_stroke_html, color_fill_
'#000000 T , '#888888'7
:lect text color based on fill
>r_f ill_rgba = Color (
color_f ill_html) .get_rgba()
color_f ill_gray = (color_f ill_rgba [ ] +
color_f ill_rgba [ 1 ] +
color_f ill_rgba [ 2 ] ) /3
>r_stroke = Color (
color_stroke_html) . get_int()
>r fill = Color (color_f ill_html ) . get_int()
iolor_f ill_gray < 0.5:
text_color = COLOR_WHITE . get_int ( )
else :
text_color = COLOR_BLACK . get_int ( )
else :
nick = ' ??? '
# XXX: should be ' ' but leave for debugging
color_stroke = COLOR_BLACK . get_int ( )
color_fill = COLOR_WHITE.get_int ()
text_color = COLOR_BLACK . get_int ( )
color = '#000000, #FFFFFF'
# Check for Right-To-Left languages:
if pango . f ind_base_dir (nick, -1) == \
pango . DIRECTION_RTL :
lang_rtl = True
else :
lang_rtl = False
# Check if new message box or add text to previous:
new_msg = True
if self ._last_msg_sender :
if not status_message :
if buddy == self ._last_msg_sender :
# Add text to previous message
new_msg = False
if not new_msg:
rb = self ._last_msg
msg_vbox = rb . get_children ( ) [1]
msg_hbox = hippo . CanvasBox (
113
orient at ion=hippo . ORIENTATION_HORIZONTAL)
msg_vbox . append (msg_hbox)
else :
rb = CanvasRoundBox (
background_color=color_f ill,
border_color=color_stroke,
padding=4 )
rb . props .border_color = color_stroke
self ._last_msg = rb
self ._last_msg_sender = buddy
if not status message:
name = hippo . CanvasText (text=nick+ ' : ',
color=text_color ,
f ont_desc=FONT_BOLD . get_pango_desc ( ) )
name_vbox = hippo . CanvasBox (
orient at ion=hippo . ORIENTATION_VERTICAL)
name_vbox . append (name)
rb . append (name_vbox)
msg_vbox = hippo . CanvasBox (
orientation=hippo.ORIENTATION_VERTICAL)
rb . append (msg_vbox)
msg_hbox = hippo . CanvasBox (
orient at ion=hippo . ORIENTATION_HORIZONTAL)
msg_vbox . append (msg_hbox)
if status_message :
self .lastrasgsender = None
if text:
message = hippo . CanvasText (
text=text ,
s i z e_mode=hippo . CANVAS_S I ZE_WRAP_WORD ,
color=text_color ,
f ont_desc=FONT_NORMAL . get_pango_desc ( ) ,
xalign=hippo.ALIGNMENT_START)
msg_hbox . append (message)
# Order of boxes for RTL languages :
if lang_rtl :
msg_hbox . reverse ( )
if new_msg:
rb . reverse ( )
if new_msg:
box = hippo . CanvasBox (padding=2 )
box . append (rb)
self. conversation. append (box)
def entry_activate_cb (self , entry):
text = entry .props . text
logger . debug (' Entry : %s ' % text)
if text:
self . add_text (self . owner , text)
entry . props . text = ' '
if self . text_channel :
self. text_channel. send (text)
else :
logger . debug (
114
'Tried to send message but text '
'channel not connected.')
And this is what the Activity looks like in action:
F
Mini Chat Activity
Jane Austen is here
James: Hello Jane.
How are you?
Jane Austen: As well as can be expected. And you?
Try launching more than one copy of sugar-emulator, with this Activity installed in
each. If you're using Fedora 10 and SUGARPROFILE the Activity does not need to be
installed more than once, but if you're using a later version of Sugar that requires
separate Linux userids for each instance you'll need to maintain separate copies of the
code for each user. In your own projects using a central Git repository at
git.sugarlabs.org will make this easy. You just do a git push to copy your changes to
the central repository and a git pull to copy them to your second userid. The second
userid can use the public URL. There's no need to set up SSH for any user other than
the primary one.
You may have read somewhere that you can install an Activity on one machine and
share that Activity with another that does not have the activity installed. In such a case
the second machine would get a copy of the Activity from the first machine and install it
automatically. You may have also read that if two users of a shared Activity have
different versions of that Activity then the one who has the newer version will
automatically update the older. Neither statement is true now or is likely to be true in
the near future. These ideas are discussed on the mailing lists from time to time but
there are practical difficulties to overcome before anything like that could work, mostly
having to do with security. For now both users of a shared Activity must have the
Activity installed. On the other hand, depending on how the Activity is written two
different versions of an Activity may be able to communicate with one another. If the
messages they exchange are in the same format there should be no problem.
115
Once you have both instances of sugar-emulator going you can launch MiniChat on one
and invite the second user to Join the Chat session. You can do both with the
Neighborhood panes of each instance. Making the invitation looks like this:
Make friend
Accepting it looks like this:
Mini Chat Activity
c*
After you've played with MiniChat for awhile come back and we'll discuss the secrets
of using Telepathy to create a shared Activity.
116
Know who Your Buddies Are
XMPP, as we said before, is the Extended Messaging and Presence Protocol. !
Presence is just what it sounds like; it handles letting you know who is available to
share your Activity, as well as what other Activities are available to share. There are
two ways to share your Activity. The first one is when you change the Share with:
pulldown on the standard toolbar so it reads My Neighborhood instead of Private.
That means anyone on the network can share your Activity. The other way to share is
to go to the Neighborhood view and invite someone specific to share. The person
getting the invitation has no idea of the invitation was specifically for him or broadcast
to the Neighborhood. The technical term for persons sharing your Activity is Buddies.
The place where Buddies meet and collaborate is called an MUC or Multi User
Chatroom.
The code used by our Activity for inviting Buddies and joining the Activity as a Buddy
is in the init method:
if self ._shared_activity :
# we are joining the activity
self.connect('joined', self ._j oined_cb)
if self . get_shared ( ) :
# we have already joined
self . _j oined_cb ( )
else :
# we are creating the activity
if not self .metadata or self .metadata . get (
' share-scope ' ,
SCOPE_PRIVATE) == SCOPE_PRIVATE :
# if we are in private session
self ._alert (_( 'Off-line* ) ,
_( 'Share, or invite someone.'))
self .connect ( 'shared' , self . _shared_cb)
def _shared_cb ( self , activity):
logger . debug (' Chat was shared')
self ._setup ( )
def _j oined_cb ( self , activity):
"""Joined a shared activity."""
if not self ._shared_activity :
return
logger . debug (' Joined a shared chat')
for buddy in \
self ._shared_activity . get_j oined_buddies () :
self . _buddy_already_exists (buddy)
self ._setup ( )
def _setup ( self ) :
self . text_channel = TextChannelWrapper (
self . _shared_activity . telepathy_text_chan,
self . _shared_activity . telepathy_conn)
117
self . text_channel . set_received_callback (
self ._received_cb)
self._alert (_( 'On-line' ) , _( 'Connected' ) )
self . _shared_activity .connect ( 'buddy-joined' ,
self ._buddy_j oined_cb)
self . _shared_activity .connect ( 'buddy-left' ,
self . _buddy_lef t_cb)
self. entry. set_sensitive (True)
self . entry . grab_focus ()
There are two ways to launch an Activity: as the first user of an Activity or by joining
an existing Activity. The first line above in bold determines whether we are joining or
are the first user of the Activity. If so we ask for the _joined_cb() method to be run when
the 'joined' event occurs. This method gets a buddy list from the _shared_activity object
and creates messages in the user interface informing the user that these buddies are
already in the chat room. Then it runs the _setup() method.
If we are not joining an existing Activity then we check to see if we are currently sharing
the Activity with anyone. If we aren't we pop up a message telling the user to invite
someone to chat. We also request that when the 'shared' even happens the _shared_cb()
method should run. This method just runs the _setup() method.
The _setup() method creates a TextChannelWrapper object using the code in
textchannel.py. It also tells the _shared_activity object that it wants some callback
methods run when new buddies join the Activity and when existing buddies leave the
Activity. Everything you need to know about your buddies can be found in the code
above, except how to send messages to them. For that we use the Text Channel.
There is no need to learn about the Text Channel in great detail because the
TextChannelWrapper class does everything you'll ever need to do with the TextChannel
and hides the details from you.
def entry_activate_cb (self , entry):
text = entry .props . text
logger . debug (' Entry : %s ' % text)
if text:
self . add_text (self . owner , text)
entry . props . text = ' '
if self . text_channel :
self. text_channel. send (text)
else :
logger . debug (
'Tried to send message but text '
'channel not connected.')
The add_text() method is of interest. It takes the owner of the message and figures out
what colors belong to that owner and displays the message in those colors. In the case
of messages sent by the Activity it gets the owner like this in the init () method:
self. owner = self .pservice . get_owner ( )
118
In the case of received messages it gets the buddy the message came from:
def _received_cb ( self , buddy, text) :
"""Show message that was received."""
if buddy:
nick = buddy .props . nick
else :
nick = ' ??? '
logger . debug (' Received message from %s: %s',
nick, text)
self . add_text (buddy, text)
But what if we want to do more than just send text messages back and forth? What do
we use for that?
It's A Series Of Tubes!
No, not the Internet. Telepathy has a concept called Tubes which describes the way
instances of an Activity can communicate together. What Telepathy does is take the
Text Channel and build Tubes on top of it. There are two kinds of Tubes:
• D-Bus Tubes
• Stream Tubes
A D-Bus Tube is used to enable one instance of an Activity to call methods in the
Buddy instances of the Activity. A Stream Tube is used for sending data over
Sockets, for instance for copying a file from one instance of an Activity to another. A
Socket is a way of communicating over a network using Internet Protocols. For instance
the HTTP protocol used by the World Wide Web is implemented with Sockets. In the
next example we'll use HTTP to transfer books from one instance of Read Etexts III to
another.
Read Etexts III, Now with Book Sharing!
The Git repository with the code samples for this book has a file named
ReadEtextsActivity3.py in the Making Shared_Activities directory which looks like
this:
import sys
import os
import logging
import tempfile
import time
import zipfile
import pygtk
import gtk
import pango
119
import dbus
import gobject
import telepathy
from sugar . activity import activity
from sugar . graphics import style
from sugar import network
from sugar . datastore import datastore
from sugar . graphics . alert import NotifyAlert
from toolbar import ReadToolbar, ViewToolbar
from gettext import gettext as _
page=0
PAGE_SIZE =45
TOOLBAR_READ = 2
logger = logging . getLogger (' read-etexts2-activity ' )
class ReadHTTPRequestHandler (
network. Chun kedGlibHTTPRequestHandler) :
"""HTTP Request Handler for transferring document
while collaborating.
RequestHandler class that integrates with Glib
mainloop. It writes the specified file to the
client in chunks, returning control to the
mainloop between chunks .
def translate_path (self , path):
"""Return the filepath to the shared document."""
return self . server . filepath
class ReadHTTPServer (network. GlibTCPServer) :
"""HTTP Server for transferring document while
collaborating. '
def init (self, server_address , filepath) :
"""Set up the GlibTCPServer with the
ReadHTTPRequestHandler .
filepath -- path to shared document to be served.
ii ii ii
self . filepath = filepath
network. GlibTCPServer . init (self,
server address , ReadHTTPRequestHandler)
class ReadURLDownloader (network . GlibURLDownloader) :
" " "URLDownloader that provides content-length and
content-type . " " "
def get_content_length (self ) :
"""Return the content-length of the download."""
if self._info is not None:
return int (self. _inf o . headers. get (
'Content-Length' ) )
def get_content_type (self ) :
120
"""Return the content-type of the download."""
if self._info is not None:
return self . _inf o .headers. get ( 'Content-type' )
return None
READ_STREAM_SERVICE = ' read-etexts-activity-http '
class ReadEtextsActivity (activity .Activity) :
def init (self, handle) :
"The entry point to the Activity"
global page
activity .Activity . init (self, handle)
self . f ileserver = None
self . ob j ect_id = handle . obj ect_id
toolbox = activity . ActivityToolbox (self )
activity_toolbar = toolbox . get_activity_toolbar ( )
activity_toolbar . keep .props . visible = False
self . edit_toolbar = activity . EditToolbar ( )
self . edit_toolbar . undo .props . visible = False
self . edit_toolbar . redo .props . visible = False
self . edit_toolbar . separator .props . visible = False
self . edit_toolbar . copy . set_sensitive (False)
self . edit_toolbar .copy. connect ( 'clicked' ,
self . edit_toolbar_copy_cb)
self . edit_toolbar . paste .props . visible = False
toolbox . add_toolbar (_ ( ' Edit ' ) , self. edit_toolbar )
self . edit_toolbar . show ( )
self . read_toolbar = ReadToolbar ( )
toolbox . add_toolbar (_ ( ' Read ' ) , self. read_toolbar )
self . read_toolbar .back. connect ( 'clicked' ,
self . go_back_cb)
self . read_toolbar .forward. connect ( 'clicked' ,
self . go_f orward_cb)
self . read_toolbar . num_page_entry .connect ( 'activate' ,
self . num_page_entry_activate_cb)
self . read_toolbar . show ( )
self . view_toolbar = ViewToolbar ( )
toolbox . add_toolbar (_ ( ' View ' ) , self . view_toolbar )
self . view_toolbar .connect ( 'go-fullscreen' ,
self . view_toolbar_go_f ullscreen_cb)
self . view_toolbar . zoom_in .connect ( 'clicked' ,
self . zoom_in_cb)
self . view_toolbar . zoom_out .connect ( 'clicked' ,
self . zoom_out_cb)
self . view_toolbar . show ( )
self . set_toolbox (toolbox)
toolbox . show ( )
self . scrolled_window = gtk . ScrolledWindow ( )
self. scrolled_window. set_policy (gtk. POLICY_NEVER,
gtk. POLICY_AUTOMATIC)
self . scrolled_window .props . shadow_type = \
gtk.SHADOW_NONE
121
self . textview = gtk . TextView ( )
self .textview. set_edi table (False)
self. textview. set_cursor_visible (False)
self. textview. set_lef t_margin (50 )
self. textview. connect ( "key_press_event" ,
self . keypress_cb)
self . progressbar = gtk . ProgressBar ( )
self.progressbar. set_orientation (
gtk. PROGRESS_LEFT_TO_RIGHT)
self.progressbar.set_fraction(0.0)
self . scrolled_window . add (self. textview)
self. textview. show()
self . scrolled_window . show ( )
vbox = gtk.VBox()
vbox . pack_start (self.progressbar, False,
False, 10)
vbox . pack_start (self . scrolled_window)
self . set_canvas (vbox)
vbox . show ( )
page =
self . clipboard = gtk . Clipboard (
display=gtk . gdk . display_get_def ault () ,
select ion=" CLIPBOARD")
self. textview. grab_ focus ( )
self . f ont_desc = pango . FontDescription ( "sans %d" %
style . zoom ( 10 ) )
self. textview . modif y_f ont (self . f ont_desc)
buffer = self . textview . get_buffer ( )
self .markset_id = buff er . connect ( "mark-set" ,
self .mark_set_cb)
self . toolbox . set_current_toolbar (TOOLBAR_READ)
self . unused_download_tubes = set ()
self . want_document = True
self . download_content_length =
self . download_content_type = None
# Status of temp file used for write_file:
self . tempf ile = None
self . close_requested = False
self. connect ("shared", self. shared_cb)
self . is_received_document = False
if self ._shared_activity and \
handle . obj ect_id == None:
# We're joining, and we don't already have
# the document.
if self . get_shared ( ) :
# Already joined for some reason, just get the
# document
self . j oined_cb (self)
else :
122
# Wait for a successful join before trying to get
# the document
self. connect ("joined", self.j oined_cb)
def keypress_cb ( self , widget, event):
"Respond when the user presses one of the arrow keys"
keyname = gtk . gdk . keyval_name (event . keyval)
print keyname
if keyname == 'plus' :
self . f ont_increase ( )
return True
if keyname == 'minus' :
self . f on t_de crease ( )
return True
if keyname == ' Page_Up ' :
self . page_previous ()
return True
if keyname == ' Page_Down ' :
self . page_next ( )
return True
if keyname == 'Up' or keyname == ' KP_Up ' \
or keyname == 'KP_Left' :
self . scroll_up ( )
return True
if keyname == 'Down' or keyname == ' KP_Down ' \
or keyname == 'KP_Right' :
self . scroll_down ( )
return True
return False
def num_page_entry_activate_cb (self , entry):
global page
if entry . props . text :
new_page = int (entry .props . text ) - 1
else :
new_page =
if new_page >= self . readtoolbar . total_pages :
new_page = self . readtoolbar . total_pages - 1
elif new_page < 0:
new_page =
self . read_toolbar . current_page = newpage
self . read_toolbar . set_current_page (new_page)
self . show_page (new_page)
entry . props . text = str (newpage + 1)
self . read_toolbar . update navbuttons ( )
page = new_page
def go_back_cb ( self , button) :
self . page_previous ()
def go_f orward_cb ( self , button):
self . page_next ( )
def page_previous ( self ) :
global page
page=page-l
123
if page < 0: page=0
self . readtoolbar . set_current_page (page)
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . upper - \
v_adj ustment .page_size
def page_next ( self ) :
global page
page=page+l
if page >= len (self .page_index) : page=0
self . readtoolbar . set_current_page (page)
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . lower
def zoom_in_cb (self , button) :
self . f ont_increase ( )
def zoom_out_cb (self , button) :
self . f on t_de crease ( )
def f ont_decrease (self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size - 1
if font_size < 1:
font_size = 1
self . f ont_desc . set_size ( f ont_size * 1024)
self .textview . modif y_f ont (self . f ont_desc)
def f ont_increase (self ) :
font^size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size + 1
self . f ont_desc . set_size ( f ont_size * 1024)
self. textview .modif y_f ont (self . f ont_desc)
def mark_set_cb (self , textbuffer, iter, textmark) :
if textbuf f er . get_has_selection ( ) :
begin, end = textbuf fer . get_selection_bounds ( )
self . edit_toolbar . copy . set_sensitive (True)
else :
self . edit_toolbar . copy . set_sensitive (False)
def edit_toolbar_copy_cb (self , button):
textbuffer = self . textview . get_buf fer ( )
begin, end = textbuf fer . get_selection_bounds ( )
copy_text = textbuf fer . get_text (begin, end)
self. clipboard. set_text (copy_text )
def view_toolbar_go_f ullscreen_cb (self , view_toolbar ) :
self. fullscreen ()
def scroll_down (self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
124
if v_adj ustment . value == v_adjustment . upper - \
v_adjustment .page_size:
self . page_next ( )
return
if v_adj ustment . value < v_adj ustment . upper - \
v_adj ustment .page_size:
new_value = v_adjustment . value + \
v_adj ustment . step_increment
if new_value > v_adjustment . upper - \
v_adj ustment .page_size:
new value = v adjustment . upper - \
v_adj ustment .page_size
v adj ustment . value = new value
def scroll_up ( self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . lower :
self . page_previous ()
return
if v_adj ustment . value > v_adjustment . lower :
new value = v adjustment . value - \
v_adj ustment . step_increment
if new_value < v_adjustment . lower :
new_value = v_adjustment . lower
v adj ustment . value = new value
def show_page ( self , pagenumber) :
global PAGE_SIZE, current_word
position = self . page_index [pagenumber ]
self . etext_f ile . seek (position)
linecount =
label_text = '\n\n\n'
textbuffer = self . textview . get_buf f er ( )
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + Unicode (line,
'iso-8859-1')
linecount = linecount + 1
label_text = label_text + '\n\n\n'
textbuffer. set_text (label_text )
self. textview. set_buf f er (textbuffer)
def save_extracted_f ile (self , zipfile, filename) :
"Extract the file to a temp directory for viewing'
filebytes = zipfile . read ( filename)
outfn = self .make_new_f ilename ( filename)
if (outfn == ' ' ) :
return False
f = open (os .path. join (self . get_activity_root () ,
'tmp', outfn), 'w')
try :
f. write (filebytes)
finally:
f . close ( )
def get_saved_page_number (self ) :
global page
125
title = self .metadata . get (' title ' , '')
if title == ' ' or not \
title[len(title)-l] . isdigit ( ) :
page =
else :
i = len(title) - 1
newPage = ' '
while (title[i] .isdigit() and i > 0) :
newPage = title [i] + newPage
i = i - 1
if title [i] == ' P' :
page = int (newPage) - 1
else :
# not a page number; maybe a
# volume number,
page =
def save_page_number (self ) :
global page
title = self .metadata . get (' title ' , '')
if title == ' ' or not \
title [len (title) - 1 ]. isdigit () :
title = title + ' P' + str(page + 1)
else :
i = len(title) - 1
while (title[i] .isdigit() and i > 0) :
i = i - 1
if title [i] == ' P' :
title = title[0:i] + 'P' + str (page + 1)
else :
title = title + ' P' + str(page + 1)
self .metadata [' title ' ] = title
def read_f ile ( self , filename) :
"Read the Etext file"
global PAGE_SIZE, page
tempfile = os .path . j oin (self . get_activity_root () ,
'instance', 'tmp%i' % time . time () )
os . link ( filename, tempfile)
self . tempfile = tempfile
if zipf ile . is_zipf ile ( filename) :
self.zf = zipf ile . ZipFile (filename, 'r')
self . book_f iles = self . zf . namelist ( )
self . save_ext r act ed_ file (self . zf ,
self .book_filesT0] )
currentFileName = os .path . j oin (
self . get_activity_root () ,
' tmp ' , self .book_f iles [ ] )
else :
currentFileName = filename
self . etext_f ile = open (currentFileName, "r" )
self . page_index = [ ]
pagecount =
linecount =
while self. etext file:
126
line = self . etext_f ile . readline ( )
if not line:
break
linecount = linecount + 1
if linecount >= PAGE_SIZE:
position = self . etext_f ile . tell ( )
self . page_index . append (position)
linecount =
pagecount = pagecount + 1
if filename . endswith (". zip" ) :
os . remove (currentFileName)
self . get_saved_page_number ( )
self . show_page (page)
self . read_toolbar . set_total_pages (
pagecount + 1)
self . read_toolbar . set_current_page (page)
# We've got the document, so if we're a shared
# activity, offer it
if self . get_shared ( ) :
self . watch_f or_tubes ()
self . share_document ( )
def make_new_f ilename ( self , filename) :
partition_tuple = f ilename . rpartition ('/' )
return partition tuple [2]
def write_f ile (self , filename) :
"Save meta data for the file."
if self . is_received_document :
# This document was given to us by someone, so
# we have to save it to the Journal .
self . etext_f ile . seek ( )
filebytes = self . etext_f ile . read ( )
f = open ( filename, ' wb ' )
try:
f. write (filebytes)
finally:
f . close ( )
elif self . tempf ile :
if self . closerequested:
os . link ( self . tempf ile, filename)
logger . debug (
"Removing temp file %s because we "
"will close",
self . tempf ile)
os.unlink(self. tempf ile)
self . tempf ile = None
else :
# skip saving empty file
raise NotlmplementedError
self .metadata [' activity ' ] = self . get_bundle_id ( )
self . save_page_number ( )
def can_close ( self ) :
self . close_requested = True
return True
127
def j oined_cb ( self , also_self ) :
"""Callback for when a shared activity is joined.
Get the shared document from another participant.
ii ii ii
self . watch_f ortubes ()
gob j ect . idle_add (self . get_document )
def get_document (self ) :
if not self . want_document :
return False
# Assign a file path to download if one
# doesn't exist yet
if not self . _j obj ect . file_path :
path = os .path . j oin (self . get_activity_root () ,
' instance ' ,
'tmp%i' % time. time ())
else :
path = self . _j obj ect . file_path
# Pick an arbitrary tube we can try to
# download the document from
try :
tube_id = self . unused_download_tubes . pop ( )
except (ValueError, KeyError) , e:
logger . debug (
'No tubes to get the document '
'from right now: %s',
e)
return False
# Avoid trying to download the document multiple
# timesat once
self . want_document = False
gob j ect . idle_add (self . download_document , tube_id, path)
return False
def download_document (self , tube_id, path):
chan = self ._shared_activity . telepathy_tubes_chan
iface = chanTtelepathy.CHANNEL_TYPE_TUBES]
addr = if ace . AcceptStreamTube (tube_id,
telepathy. S0CKET_ADDRESS_TYPE_IPV4 ,
telepathy . SOCKET_ACCESS_CONTROL_LOCALHOST, 0,
utf8_strings=True)
logger . debug (' Accepted stream tube: '
'listening address is %r',
addr)
assert isinstance (addr, dbus. Struct)
assert len (addr) == 2
assert isinstance (addr [ ] , str)
assert isinstance (addr [ 1 ] , (int, long))
assert addr[l] > and addr[l] < 65536
port = int(addr[l])
self.progressbar.show()
getter = ReadURLDownloader (
128
"http : //%s : %d/ document"
% (addr [0] , port) )
getter. connect ("finished",
self . download_result_cb, tube_id)
getter. connect ("progress",
self . download_progress_cb, tube_id)
getter. connect ("error",
self . download_error_cb, tube_id)
logger . debug ( "Starting download to %s...", path)
getter. start (path)
self . download_content_length = \
getter . get_content_length ( )
self . download_content_type = \
getter . get_content_type ( )
return False
def download_progress_cb (self , getter,
bytes_downloaded, tube_id) :
if self . download_content_length > 0:
logger . debug (
"Downloaded %u of %u bytes from tube %u. . ."
bytes_downloaded,
self . download_content_length,
tube_id)
else :
logger . debug ( "Downloaded %u bytes from tube %u.
bytes_downloaded, tube_id)
total = self . download_content_length
self . set_downloaded_bytes (bytes_down loaded, total )
gtk . gdk . threads_enter ( )
while gtk . events_pending ( ) :
gtk . main_iteration ( )
gtk . gdk . threads_leave ( )
def set_downloaded_bytes (self , bytes, total):
fraction = float (bytes) / float (total)
self.progressbar. set_fraction (fraction)
logger . debug ( "Downloaded percent", fraction)
def clear_downloaded_bytes (self ) :
self.progressbar. set_fraction (0 . 0)
logger . debug ( "Cleared download bytes")
def download_error_cb ( self , getter, err, tube_id) :
self . progressbar . hide ()
logger . debug (
"Error getting document from tube %u: %s",
tube_id, err)
self. alert (_(' Failure') ,
_( 'Error getting document from tube'))
self . want_document = True
self . download_content_length =
self . download_content_type = None
gobj ect . idle_add ( self . get_document )
def download_result_cb (self , getter, tempfile,
suggested_name, tube_id) :
if self . download_content_type .startswith(
129
'text/html' ) :
# got an error page instead
self . download_error_cb (getter,
'HTTP Error', tube_id)
return
del self . unused_download_tubes
self . tempf ile = tempfile
file_path = os .path . j oin (self . get_activity_root () ,
'instance', '%i' % time . time () )
logger . debug (
"Saving file %s to datastore . . . " , file_path)
os . link (tempf ile, file_path)
self . _j ob j ect . f ile_path = file_path
datastore . write (self . _j obj ect ,
transf er_ownership=True)
logger . debug (
"Got document %s (%s) from tube %u",
tempfile, suggested_name, tube_id)
self . is_received_document = True
self . read_f ile (tempfile)
self . save ( )
self . progressbar . hide ()
def shared_cb ( self , activityid) :
"""Callback when activity shared.
Set up to share the document .
# We initiated this activity and have now shared it,
# so by definition we have the file,
logger . debug (' Activity became shared')
self . watch_f ortubes ()
self . share_document ( )
def share_document (self ) :
"""Share the document."""
h = hash ( self ._activity_id)
port = 1024 + Th % 64511)
logger . debug (
'Starting HTTP server on port %d', port)
self . f ileserver = ReadHTTPServer ( ( " " , port),
self . tempfile)
# Make a tube for it
chan = self ._shared_activity . telepathy_tubes_chan
iface = chan [telepathy. CHANNEL_TYPE_TUBES]
self . f ileserver_tube_id = if ace . Of f erStreamTube (
READ_STREAM_SERVICE,
U,
telepathy. SOCKET_ADDRESS_TYPE_IPV4 ,
('127.0.0.1', dbus . UIntl6 (port) ) ,
telepathy. SOCKET_ACCESS_CONTROL_LOCALHOST,
0)
130
def watch_f or_tubes ( self ) :
"""Watch for new tubes."""
tubes_chan = self ._shared_activity . telepathy_tubes_chan
tubes_chan [telepathy . CHANNEL_TYPE_TUBES] . \
connect_to_signal (
' NewTube ' , self . new_tube_cb)
tubes_chan [telepathy . CHANNEL_TYPE_TUBES] .ListTubes (
reply_handler=self . list_tubes_reply_cb,
error_handler=self . list_tubes_error_cb)
def new_tube_cb ( self , tube_id, initiator, tube_type,
service, params, state) :
"""Callback when a new tube becomes available."""
logger . debug (
'New tube: ID=%d initator=%d type=%d service=%s '
'params=%r state=%d', tube_id, initiator,
tube_type, service, params, state)
if service == READ_STREAM_SERVICE :
logger . debug (' I could download from that tube')
self . unused_download_tubes . add (tube_id)
# if no download is in progress, let's fetch
# the document
if self . want_document :
gob j ect . idle_add (self . get_document )
def list_tubes_reply_cb (self , tubes):
"""Callback when new tubes are available."""
for tube_info in tubes:
self . new_tube_cb ( *tube_inf o)
def list_tubes_error_cb (self , e):
"""Handle ListTubes error by logging."""
logger . error (' ListTubes ( ) failed: %s ' , e)
def alert (self, title, text=None) :
alert = Notif yAlert (timeout=20 )
alert . props . title = title
alert . props .msg = text
self . add_alert (alert)
alert. connect ('response', self. alert_cancel_cb)
alert . show ( )
def alert_cancel_cb ( self , alert, response_id) :
self . remove_alert (alert)
self.textview. grab_f ocus ( )
The contents of activity.info are these lines:
[Activity]
name = Read Etexts III
service_name = net . f lossmanuals . ReadEtextsActivity
icon = read-etexts
exec = sugar-activity ReadEtextsActivity3 . ReadEtextsActivity
show_launcher = no
activity version = 1
mime_types = text/plain; application/zip
license = GPLv2+
131
To try it out, download a Project Gutenberg book to the Journal, open it with this latest
Read Etexts III, then share it with a second user who has the program installed but not
running. She should accept the invitation to join that appears in her Neighborhood
view. When she does Read Etexts II will start up and copy the book from the first user
over the network and load it. The Activity will first show a blank screen, but then a
progress bar will appear just under the toolbar and indicate the progress of the copying.
When it is finished the first page of the book will appear.
So how does it work? Let's look at the code. The first points of interest are the class
definitions that appear at the beginning: ReadHTTPRequestHandler,
ReadHTTPServer, and ReadURLDownloader. These three classes extend (that is to
say, inherit code from) classes provided by the sugar.network package. These classes
provide an HTTP client for receiving the book and an HTTP Server for sending the
book.
This is the code used to send a book:
def shared_cb ( self , activityid) :
"""Callback when activity shared.
Set up to share the document .
# We initiated this activity and have now shared it,
# so by definition we have the file.
logger . debug (' Activity became shared')
self . watch_f ortubes ()
self . share_document ( )
def share_document (self ) :
"""Share the document."""
h = hash ( self ._activity_id)
port = 1024 + Th % 64511)
logger . debug (
'Starting HTTP server on port %d', port)
self . f ileserver = ReadHTTPServer (("" , port),
self . tempf ile)
# Make a tube for it
chan = self ._shared_activity . telepathy_tubes_chan
iface = chan [telepathy. CHANNEL_TYPE_TUBES]
self . f ileserver_tube_id = if ace . Of f erStreamTube (
READ_STREAM_SERVICE,
U,
telepathy. SOCKET_ADDRESS_TYPE_IPV4,
('127.0.0.1', dbus.UIntl6 (port) ) ,
telepathy. SOCKET_ACCESS_CONTROL_LOCALHOST,
0)
132
You will notice that a hash of the _activity _id is used to get a port number. That port is
used for the HTTP server and is passed to Telepathy, which offers it as a Stream
Tube. On the receiving side we have this code:
def j oined_cb ( self , also_self ) :
"""Callback for when a shared activity is joined.
Get the shared document from another participant.
ii ii ii
self . watch_f or_tubes ()
gobj ect . idle_add ( self . get_document )
def get_document ( self ) :
if not self . want_document :
return False
# Assign a file path to download if one doesn't
# exist yet
if not self . _j obj ect . file_path :
path = os .path. join (self . get_activity_root () ,
' instance ' ,
' tmp%i' % time. time () )
else :
path = self . _j obj ect . file_path
# Pick an arbitrary tube we can try to download the
# document from
try :
tube_id = self . unused_download_tubes . pop ( )
except (ValueError, KeyError) , e:
logger . debug (
'No tubes to get the document from '
' right now : %s ' ,
e)
return False
# Avoid trying to download the document multiple
# times at once
self . want_document = False
gobj ect . idle_add ( self . download_document ,
tube_id, path)
return False
def download_document ( self , tube_id, path):
chan = self ._shared_activity . telepathy_tubes_chan
iface = chan [telepathy. CHANNEL_TYPE_TUBES]
addr = if ace . AcceptStreamTube (tube_id,
telepathy. S0CKET_ADDRESS_TYPE_IPV4 ,
telepathy. SOCKET_ACCESS_CONTROL_LOCALHOST,
0,
utf8_strings=True)
logger . debug (
'Accepted stream tube: listening address is %r',
addr)
assert isinstance (addr, dbus. Struct)
assert len (addr) == 2
assert isinstance (addr [ ] , str)
133
assert isinstance (addr [ 1 ] , (int, long))
assert addrfl] > and addr[l] < 65536
port = int(addr[l])
self .progressbar.showf)
getter = ReadURLDownloader (
"http: //%s:%d/document"
% (addr [0] , port) )
getter. connect ("finished",
self . download_result_cb, tube_id)
getter. connect ("progress",
self . download_progress_cb, tube_id)
getter. connect ("error",
self . download_error_cb, tube_id)
logger . debug (
"Starting download to %s...", path)
getter. start (path)
self . download_content_length = \
getter . get_content_length ( )
self . download_content_type = \
getter . get_content_type ( )
return False
def download_progress_cb (self , getter,
bytes_downloaded, tube_id) :
if self . download_content_length > 0:
logger . debug (
"Downloaded %u of %u bytes from tube %u.
bytes_downloaded,
self . download_content_length,
tube_id)
else :
logger . debug (
"Downloaded %u bytes from tube %u...",
bytes_downloaded, tube_id)
total = self . download_content_length
self . set_downloaded_bytes (bytes_downloaded,
total)
gtk . gdk . threads_enter ( )
while gtk . events_pending ( ) :
gtk .main_iteration ( )
gtk . gdk . threads_leave ()
def download_error_cb (self , getter, err, tube_id) :
self . progressbar . hide ()
logger . debug (
"Error getting document from tube %u: %s",
tube_id, err)
self. alert (_(' Failure') ,
_(' Error getting document from tube'))
self . want_document = True
self . download_content_length =
self . download_content_type = None
gob j ect . idle_add (self . get_document )
def download_result_cb (self , getter, tempfile,
suggested name, tube_id) :
if self . download_content_type . startswith (
134
'text/html ' ) :
# got an error page instead
self . download_error_cb (getter,
'HTTP Error', tube_id)
return
del self . unused_download_tubes
self . tempf ile = tempfile
file_path = os . path . j oin (self . get_activity_root () ,
' instance ' ,
'%i ' % time. time () )
logger . debug (
"Saving file %s to datastore . . . " , file_path)
os . link (tempf ile, file_path)
self . _j ob j ect . f ile_path = file_path
datastore. write (self._jobject,
transf er_ownership=True)
logger . debug (
"Got document %s (%s) from tube %u",
tempfile, suggested_name, tube_id)
self . is_received_document = True
self . read_f ile (tempfile)
self . save ( )
self . progressbar . hide ()
Telepathy gives us the address and port number associated with a Stream Tube and we
set up the HTTP Qient to read from it. The client reads the file in chunks and calls
download_progress_cb() after every chunk so we can update a progress bar to show the
user how the download is progressing. There are also callback methods for when there
is a download error and for when the download is finished,
The ReadURLDownloader class is not only useful for transferring files over Stream
Tubes, it can also be used to interact with websites and web services. My Activity Get
Internet Archive Books uses this class for that purpose.
The one remaining piece is the code which handles getting Stream Tubes to download
the book from. In this code, adapted from the Read Activity, as soon as an instance of
an Activity receives a book it turns around and offers to share it, thus the Activity may
have several possible Tubes it could get the book from:
READ_STREAM_SERVICE = ' read-etexts-activity-http '
def watch_f or_tubes ( self ) :
"""Watch for new tubes.
tubes_chan = self ._shared_activity . \
telepathy_tubes_chan
tubes_chan [telepathy . CHANNEL_TYPE_TUBES] . \
connect_to_signal (
135
'NewTube ' ,
self . new_tube_cb)
tubes_chan [telepathy . CHANNEL_TYPE_TUBES] . \
ListTubes (
reply_handler=self . list_tubes_reply_cb,
error_handler=self . list_tubes_error_cb)
def new_tube_cb (self , tube_id, initiator,
tube_type, service, params, state) :
"""Callback when a new tube becomes available."""
logger . debug (
'New tube: ID=%d initator=%d type=%d service=%s '
'params=%r state=%d', tube_id, initiator,
tube_type,
service, params, state)
if service == READ_STREAM_SERVICE :
logger . debug (' I could download from that tube')
self . unused_download_tubes . add (tube_id)
# if no download is in progress,
# let's fetch the document
if self . want_document :
gob j ect . idle_add (self . get_document )
def list_tubes_reply_cb (self , tubes):
"""Callback when new tubes are available."""
for tube_info in tubes:
self . new_tube_cb ( *tube_inf o)
def list_tubes_error_cb (self , e):
"""Handle ListTubes error by logging."""
logger . error (' ListTubes ( ) failed: %s', e)
The READ STREAM SERVICE constant is defined near the top of the source file.
Using D-Bus Tubes
D-Bus is a method of supporting IPC, or Inter-Process Communication, that was
created for the GNOME desktop environment. The idea of IPC is to allow two running
programs to communicate with each other and execute each other's code. GNOME uses
D-Bus to provide communication between the desktop environment and the programs
running in it, and also between GNOME and the operating system. A D-Bus Tube is
how Telepathy makes it possible for an instance of an Activity running on one
computer to execute methods in another instance of the same Activity running on a
different computer. Instead of just sending simple text messages back and forth or
doing file transfers, your Activities can be truly shared. That is, your Activity can allow
many people to work on the same task together.
136
I have never written an Activity that uses D-Bus Tubes myself, but many others have.
We're going to take a look at code from two of them: Scribble by Sayamindu Dasgupta
and Batalla Naval, by Gerard J. Cerchio and Andres Ambrois, which was written for
the Ceibal Jam.
Scribble is a drawing program that allows many people to work on the same drawing
at the same time. Instead of allowing you to choose what colors you will draw with, it
uses the background and foreground colors of your Buddy icon (the XO stick figure) to
draw with. That way, with many people drawing shapes it's easy to know who drew
what. If you join the Activity in progress Scribble will update your screen so your
drawing matches everyone else's screen. Scribble in action looks like this:
Batalla Naval is a version of the classic game Battleship. Each player has two grids: one
for placing his own ships (actually the computer places the ships for you) and another
blank grid representing the area where your opponent's ships are. You can't see his
ships and he can't see yours. You click on the opponent's grid (on the right) to indicate
where you want to aim an artillery shell. When you do the corresponding square will
light up in both your grid and your opponent's own ship grid. If the square you picked
corresponds to a square where your opponent has placed a ship that square will show
up in a different color. The object is to find the squares containing your opponent's
ships before he finds yours. The game in action looks like this:
137
[ &«aJauyralActiwtr
* o
♦♦♦♦♦♦ ^w ♦ <#► + ♦♦♦ + ♦ + ♦♦
♦ ♦
♦ ♦ ♦ w ♦ H r ♦ ♦ ♦
* -♦♦■
♦ ♦ -<
♦ ♦ <
♦ ♦♦
► ♦ ♦ ♦
► ♦ + +
♦ ♦ ♦ ^
♦♦♦♦♦♦♦♦♦<
I suggest that you download the latest code for these two Activities from Gitorious using
these commands:
mkdir scribble
cd scribble
git clone git : //git . sugarlabs . org/scribble/mainline . git
cd . .
mkdir batallanaval
cd batallanaval
git clone git : //git . sugarlabs . org/batalla-naval/mainline . git
You'll need to do some setup work to get these running in sugar-emulator. Scribble
requires the goocanvas GTK component and the Python bindings that go with it.
These were not installed by default in Fedora 10 but I was able to install them using
Add/Remove Programs from the System menu in GNOME. Batalla Naval is missing
setup.py, but that's easily fixed since every setup.py is identical. Copy the one from
the book examples into the mainline/BatallaNaval.activity directory and run
./setup.py dev on both Activities.
These Activities use different strategies for collaboration. Scribble creates lines of
Python code which it passes to all Buddies and the Buddies use exec to run the
commands. This is the code used for drawing a circle:
def process_item_f inalize (self , x, y) :
if self. tool == 'circle':
self.cmd = "goocanvas . Ellipse (
parent=self._root,
center x=%d,
138
center_y=%d, radiusx = %d,
radius_y = %d,
f ill_color_rgba = %d,
strokecolorrgba = %d,
title = '%s')" % (self . item. props . center_x,
self . item. props . center_y,
self . item. props . radiusx,
self.item.props.radius_y,
self . _f ill_color ,
self, stroke color, self. item id)
def processcmd ( self , cmd) :
#print 'Processing cmd : ' + cmd
exec (cmd)
#FIXME: Ugly hack, but I'm too lazy to
# do this nicely
if len (self . cmd_list) > 0:
self . cmd_list += (';' + cmd)
else :
self . cmd_list = cmd
The cmd_list variable is used to create a long string containing all of the commands
executed so far. When a new Buddy joins the Activity she is sent this variable to
execute so that her drawing area has the same contents as the other Buddies have.
This is an interesting approach but you could do the same thing with the TextChannel
so it isn't the best use of D-Bus Tubes. Batalla Naval's use of D-Bus is more typical.
How D-Bus Tubes Work, More Or Less
D-Bus enables you to have two running programs send messages to each other. The
programs have to be running on the same computer. Sending a message is sort of a
roundabout way of having one program run code in another. A program defines the
kind of messages it is willing to receive and act on. In the case of B atalla Naval it
defines a message "tell me what square you want to fire a shell at and I'll figure out if
part of one of my ships is in that square and tell you." The first program doesn't
actually run code in the second one, but the end result is similar. D-Bus Tubes is a way
of making D-Bus able to send messages like this to a program running on another
computer.
139
Think for a minute about how you might make a program on one computer run code in
a running program on a different computer. You'd have to use the network, of course.
Everyone is familiar with sending data over a network, but in this case you would have
to send program code over the network. You would need to be able to tell the running
program on the second computer what code you wanted it to run. You would have to
send it a method call and all the parameters you needed to pass into the method, and
you'd need a way to get a return value back.
Isn't that kind of like what Scribble is doing in the code we just looked at? Maybe we
could make our code do something like that?
Of course if you did that then every program you wanted to run code in remotely
would have to be written to deal with that. If you had a bunch of programs you wanted
to do that with you'd have to have some way of letting the programs know which
requests were meant for it. It would be nice if there was a program running on each
machine that dealt with making the network connections, converting method calls to
data that could be sent over the network and then converting the data back into method
calls and running them, plus sending any return values back. This program should be
able to know which program you wanted to run code in and see that the method call is
run there. The program should run all the time, and it would be really good if it made
running a method on a remote program as simple as running a method in my own
program.
As you might guess, what I've just described is more or less what D-Bus Tubes are.
There are articles explaining how it works in detail but it is not necessary to know how
it works to use it. You do need to know about a few things, though. First, you need to
know how to use D-Bus Tubes to make objects in your Activity available for use by
other instances of that Activity running elsewhere.
An Activity that needs to use D-Bus Tubes needs to define what sorts of messages it is
willing to act on, in effect what specific methods in in the program are available for this
use. All Activities that use D-Bus Tubes have constants like this:
SERVICE = "org . randomink . sayamindu . Scribble"
IFACE = SERVICE
PATH = " /org/randomink/sayamindu/Scribble"
140
These are the constants used for the Scribble Activity. The first constant, named
SERVICE, represents the bus name of the Activity. This is also called a well-known
name because it uses a reversed domain name as part of the name. In this case
Sayamindu Dasgupta has a website at http: //say amindu.randomink. org so he reverses
the dot-separated words of that URL to create the first part of his bus name. It is not
necessary to own a domain name before you can create a bus name. You can use
org.sugarlabs.ActivityName if you like. The point is that the bus name must be
unique, and by convention this is made easier by starting with a reversed domain
name.
The PATH constant represents the object path. It looks like the bus name with slashes
separating the words rather than periods. For most Activities that is exactly what it
should be, but it is possible for an application to expose more than one object to D-Bus
and in that case each object exposed would have its own unique name, by convention
words separated by slashes.
The third constant is IFACE, which is the interface name. An interface is a collection
of related methods and signals, identified by a name that uses the same convention
used by the bus name. In the example above, and probably in most Activities using a
D-Bus Tube, the interface name and the bus name are identical.
So what is a signal? A signal is like a method but instead of one running program
calling a method in one other running program, a signal is broadcast. In other words,
instead of executing a method in just one program it executes the same method in many
running programs, in fact in every running program that has that method that it is
connected to through the D-Bus. A signal can pass data into a method call but it can't
receive anything back as a return value. It's like a radio station that broadcasts music to
anyone that is tuned in. The flow of information is one way only.
Of course a radio station often receives phone calls from its listeners. A disc jockey
might play a new song and invite listeners to call the station and say what they thought
about it. The phone call is two way communication between the disc jockey and the
listener, but it was initiated by a request that was broadcast to all listeners. In the same
way your Activity might use a signal to invite all listeners (Buddies) to use a method to
call it back, and that method can both supply and receive information.
141
In D-Bus methods and signals have signatures. A signature is a description of the
parameters passed into a method or signal including its data types. Python is not a
strongly typed language. In a strongly typed language every variable has a data type
that limits what it can do. Data types include such things as strings, integers, long
integers, floating point numbers, booleans, etc. Each one can be used for a specific
purpose only. For instance a boolean can only hold the values True and False, nothing
else. A string can be used to hold strings of characters, but even if those characters
represent a number you cannot use a string for calculations. Instead you need to
convert the string into one of the numeric data types. An integer can hold integers up to
a certain size, and a long integer can hold much larger integers, A floating point
number is a number with a decimal point in scientific notation. It is almost useless for
business arithmetic, which requires rounded results.
In Python you can put anything into any variable and the language itself will figure out
how to deal with it. To make Python work with D-Bus, which requires strongly typed
variables that Python doesn't have, you need a way to tell D-Bus what types the
variables you pass into a method should have. You do this by using a signature string
as an argument to the method or signal. Methods have two strings: an in signature
and an out_signature. Signals just have a signature parameter. Some examples of
signature strings:
Two parameters, both integers
Three parameters, all strings
ixd
Three parameters, an integer, a long integer, and a double precision floating point number.
a(ssiii)
An array where each element of the array is a tuple containing two strings and three integers.
There is more information on signature strings in the dbus-python tutorial at
http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html .
Introducing Hello Mesh And Friends
If you study the source code of a few shared Activities you'll conclude that many of
them contain nearly identical methods, as if they were all copied from the same source.
In fact, more likely than not they were. The Activity Hello Mesh was created to be an
example of how to use D-Bus Tubes in a shared Activity. It is traditional in
programming textbooks to have the first example program be something that just prints
the words "Hello World" to the console or displays the same words in a window. In that
tradition Hello Mesh is a program that doesn't do all that much. You can find the code
in Gitorious at http://git.sugarlabs.org/projects/hello-mesh .
142
Hello Mesh is widely copied because it demonstrates how to do things that all shared
Activities need to do. When you have a shared Activity you need to be able to do two
things:
• Send information or commands to other instances of your Activity.
• Give Buddies joining your Activity a copy of the current state of the Activity.
It does this using two signals and one method:
• A signal called HelloO that someone joining the Activity sends to all participants.
The HelloO method takes no parameters.
• A method called WorldO which instances of the Activity receiving HelloO send back
to the sender. This method takes a text string as an argument, which is meant to
represent the current state of the Activity.
• Another signal called SendTextO which sends a text string to all participants. This
represents updating the state of the shared Activity. In the case of Scribble this
would be informing the others that this instance has just drawn a new shape.
Rather than study Hello Mesh itself I'd like to look at the code derived from it used in
Batalla Naval. I have taken the liberty of running the comments, originally in Spanish,
through Google Tra nsla te to make everything in English. I have also removed some
commented-out lines of code.
This Activity does something clever to make it possible to run it either as a Sugar
Activity or as a standalone Python program. The standalone program does not support
sharing at all, and it runs in a Window. The class Activity is a subclass of Window, so
when the code is run standalone the initO function in BatallaNaval.py gets a Window,
and when the same code is run as an Activity the instance of class
BatallaNavalActivity is passed to init():
from sugar . activity. activity import Activity, ActivityToolbox
import BatallaNaval
from Collaboration import CollaborationWrapper
class BatallaNavalActivity (Activity) :
' ' ' The Sugar class called when you run this
program as an Activity. The name of this
class file is marked in the
activity/activity . info file. ' ' '
def init (self, handle) :
Activity. init (self, handle)
self . gamename = 'BatallaNaval'
# Create the basic Sugar toolbar
toolbox = ActivityToolbox (self )
self. set toolbox (toolbox)
143
toolbox . show ( )
# Create an instance of the CollaborationWrapper
# so you can share the activity.
self . colaboracion = CollaborationWrapper (self )
# The activity is a subclass of Window, so it
# passses itself to the init function
BatallaNaval . init (False, self)
The other clever thing going on here is that all the collaboration code is placed in its own
CollaborationWrapper class which takes the instance of the BatallNavalActivity class
in its constructor. This separates the collaboration code from the rest of the program.
Here is the code in CollaborationWrapper.py:
import logging
from sugar . presence import presenceservice
import telepathy
from dbus. service import method, signal
# In build 656 Sugar lacks sugartubeconn
try :
from sugar . presence . sugartubeconn import \
SugarTubeConnection
except :
from sugar . presence . tubeconn import TubeConnection as \
SugarTubeConnection
from dbus . gob j ect_service import ExportedGOb j ect
' ' ' In all collaborative Activities in Sugar we are
made aware when a player enters or leaves . So that
everyone knows the state of the Activity we use
the methods Hello and World. When a participant
enters Hello sends a signal that reaches
all participants and the participants
respond directly using the method "World",
which retrieves the current state of the Activity.
After the updates are given then the signal
Play is used by each participant to make his move.
In short this module encapsulates the logic of
"collaboration" with the following effect:
- When someone enters the collaboration
the Hello signal is sent.
- Whoever receives the Hello signal responds
with World
- Every time someone makes a move he uses
the method Play giving a signal which
communicates to each participant
what his move was.
SERVICE = "org. ceibaljam. BatallaNaval"
IFACE = SERVICE
PATH = "/org/ceibaljam/BatallaNaval"
logger = logging . getLogger (' BatallaNaval '
logger. setLevel (logging. DEBUG)
144
class CollaborationWrapper (ExportedGOb j ect ) :
' ' ' A wrapper for the collaboration methods .
Get the activity and the necessary callbacks,
def init (self, activity) :
self . activity = activity
self . presence_service = \
presenceservice . get_instance ()
self. owner = \
self . presence_service . get_owner ( )
def set_up(self, buddy_j oined_cb, buddy_lef t_cb,
World_cb, Play_cb, my_boats) :
self. activity. connect ( ' shared' ,
self . _shared_cb)
if self . activity ._shared_activity :
# We are joining the activity
self. activity. connect ( ' joined' ,
self . _j oined_cb)
if self . activity . get_shared () :
# We've already joined
self . _j oined_cb ( )
self . buddy_j oined = buddy_j oined_cb
self . buddy_lef t = buddy_lef t_cb
self . World_cb = World_cb
# Called when someone passes the board state.
self.Play_cb = Play_cb
# Called when someone makes a move.
# Submitted by making World on a new partner
self .my_boats = [(b.nombre, b . orientacion,
b . largo, b . pos [ ] ,
b.posfl]) for b in my_boats]
self. world = False
self. entered = False
def _shared_cb ( self , activity):
self . _sharing_setup ( )
self . tubes_chan [telepathy . CHANNEL_TYPE_TUBES] . \
Of f erDBusTube (
SERVICE, {})
self . is_initiator = True
def _j oined_cb ( self , activity):
self . _sharing_setup ( )
self . tubes_chan [telepathy . CHANNEL_TYPE_TUBES] . \
ListTubes (
reply_handler=self . _list_tubes_reply_cb,
error_handler=self . _list_tubes_error_cb)
self . is_initiator = False
def _sharing_setup ( self ) :
if self . activity ._shared_activity is None:
logger . error (
'Failed to share or join activity')
145
return
self . conn = \
self. activity ._shared_activity . telepathy_conn
self . tubes_chan = \
self. activity ._shared_activity . telepathy_tubes_chan
self . text_chan = \
self. activity ._shared_activity . telepathy_text_chan
self .tube schan [telepathy . CHANNEL_TYPE_TUBES ] . \
connect_to_signal (
'NewTube', self ._new_tube_cb)
self. activity ._shared_activity .connect (
'buddy- j oined ' ,
self ._buddy_j oined_cb)
self. activity ._shared_activity .connect (
'buddy-left ' ,
self . _buddy_lef t_cb)
# Optional - included for example :
# Find out who's already in the shared activity:
for buddy in \
self. activity ._shared_activity . \
get_j oined_buddies () :
logger . debug (
'Buddy %s is already in the activity',
buddy .props .nick)
def participant_change_cb (self , added, removed) :
logger . debug (
'Tube: Added participants: %r', added)
logger . debug (
'Tube: Removed participants: %r', removed)
for handle, busnarae in added:
buddy = self ._get_buddy (handle)
if buddy is not None:
logger . debug (
'Tube: Handle %u (Buddy %s) was added',
handle, buddy .props . nick)
for handle in removed:
buddy = self ._get_buddy (handle)
if buddy is not None:
logger . debug (' Buddy %s was removed' %
buddy .props .nick)
if not self . entered:
if self . is_initiator :
logger . debug (
"I'm initiating the tube, "
"will watch for hellos.")
self . add_hello_handler ( )
else :
logger . debug (
'Hello, everyone! What did I miss?')
self .Hello ()
self. entered = True
146
# This is sent to all participants whenever we
# join an activity
@signal (dbus_interf ace=IFACE, signature^' ' )
def Hello (self) :
"""Say Hello to whoever else is in the tube."""
logger . debug (' I said Hello.')
# This is called by whoever receives our Hello signal
# This method receives the current game state and
# puts us in sync with the rest of the participants .
# The current game state is represented by the
# game object
@method (dbus_interf ace=IFACE, in_signature= ' a (ssiii) ',
out_signature= ' a ( ssiii) ')
def World(self, boats):
"""To be called on the incoming XO after
they Hello. """
if not self. world:
logger . debug (' Somebody called World on me')
self. world = True # Instead of loading
# the world, I am
# receiving play by
# play,
self . World_cb (boats )
# now I can World others
self . add_hello_handler ( )
else :
self. world = True
logger . debug (
"I've already been welcomed, doing nothing")
return self .my_boats
@ signal (dbus_interf ace=IFACE, signature='ii' )
def Play(self, x, y) :
"""Say Hello to whoever else is in the tube."""
logger . debug (' Running remote play:%s x %s.', x, y)
def add_hello_handler ( self ) :
logger . debug (' Adding hello handler.')
self. tube. add_signal_receiver (self . hello_signal_cb,
'Hello', IFACE,
path=PATH, sender_keyword= ' sender ' )
self. tube. add_signal_receiver (self . play_signal_cb,
'Play', IFACE,
path=PATH, sender_keyword= ' sender ' )
def hello_signal_cb ( self , sender=None) :
"""Somebody Helloed me. World them."""
if sender == self . tube . get_unique_name ( ) :
# sender is my bus name, so ignore my own signal
return
logger . debug (' Newcomer %s has joined', sender)
logger . debug (
'Welcoming newcomer and sending them '
' the game state ' )
self. other = sender
147
# I send my ships and I get theirs in return
enemy_boats = self . tube . get_obj ect (self . other ,
PATH) . World (
self .my_boats , dbus_interf ace=IFACE)
# I call the callback World, to load the enemy ships
self . World_cb (enemy_boats)
def play_signal_cb (self , x, y, sender=None) :
"""Somebody placed a stone. """
if sender == self . tube . get_unique_name ( ) :
return # sender is my bus name,
# so ignore my own signal
logger . debug (' Buddy %s placed a stone at %s x %s ' ,
sender, x, y)
# Call our Play callback
self . Play_cb (x, y)
# In theory, no matter who sent him
def _list_tubes_error_cb (self , e):
logger . error (' ListTubes ( ) failed: %s', e)
def _list_tubes_reply_cb (self , tubes):
for tube_info in tubes:
self . _new_tube_cb ( *tube_inf o)
def _new_tube_cb (self , id, initiator, type,
service, params, state) :
logger . debug (' New tube: ID=%d initator=%d '
'type=%d service=%s '
'params=%r state=%d', id, initiator, '
'type, service, params, state)
if (type == telepathy. TUBE_TYPE_DBUS and
service == SERVICE) :
if state == telepathy. TUBE_STATE_LOCAL_PENDING :
self .tube schan [telepathy . CHANNEL_TYPE_TUBES ]
.AcceptDBusTube (id)
self. tube = SugarTubeConnection (self . conn,
self .tube s_chan [telepathy . CHANNEL_TYPE_TUBES ]
id, group_iface=
self . text_chan [telepathy. \
CHANNEL_INTERFACE_GROUP] )
super (CollaborationWrapper ,
self). init (self. tube, PATH)
self . tube . watch_participants (
self . parti cipant_change_cb)
def _buddy_j oined_cb (self, activity, buddy) :
"""Called when a buddy joins the shared
activity. """
logger . debug (
'Buddy %s joined', buddy .props . nick)
if self . buddy_j oined:
self . buddy_j oined (buddy)
def _buddy_lef t_cb (self, activity, buddy) :
"""Called when a buddy leaves the shared
activity. '
148
if self . buddy_lef t :
self . buddy_lef t (buddy)
def _get_buddy ( self , cs_handle):
"""Get a Buddy from a channel specific handle."""
logger . debug (' Trying to find owner of handle %u...',
cs_handle)
group = self . text_chan [telepathy . \
CHANNEL_INTERFACE_GROUP]
my_csh = group . GetSelf Handle ( )
logger . debug (
'My handle in that group is %u ' , my_csh)
if my_csh == cs_handle:
handle = self . conn . GetSelf Handle ( )
logger . debug (' CS handle %u belongs to me, %u ' ,
cs_handle, handle)
elif group . GetGroupFlags ( ) & \
telepathy . \
CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
handle = group . GetHandleOwners ( [cs_handle] ) [0]
logger . debug (' CS handle %u belongs to %u',
cs_handle, handle)
else :
handle = cs_handle
logger . debug (' non-CS handle %u belongs to itself',
handle)
# XXX: deal with failure to get the handle owner
assert handle !=
return self . presence service. \
get_buddy_by_telepathy_handle (
self. conn. service_name,
self . conn . obj ect_path, handle)
Most of the code above is similar to what we've seen in the other examples, and most of
it can be used as is in any Activity that needs to make D-Bus calls. For this reason we'll
focus on the code that is specific to using D-Bus. The logical place to start is the HelloO
method. There is of course nothing magic about the name "Hello". Hello Mesh is
meant to be a "Hello World" program for using D-Bus Tubes, so by convention the
words "Hello" and "World" had to be used for something. The HelloO method is
broadcast to all instances of the Activity to inform them that a new instance is ready to
receive information about the current state of the shared Activity. Your own Activity
will probably need something similar, but you should feel free to name it something
else, and if you're writing the code for a school assignment you should definitely name
it something else:
# This is sent to all participants whenever we
# join an activity
@signal (dbus_interf ace=IFACE, signature^' ' )
def Hello (self) :
"""Say Hello to whoever else is in the tube."""
logger . debug (' I said Hello.')
def add_hello_handler ( self ) :
logger . debug (' Adding hello handler.')
149
self . tube . add_signal_receiver (
self . hello_signal_cb,
'Hello', IFACE,
path=PATH, sender_keyword= ' sender ' )
def hello_signal_cb (self , sender=None) :
"""Somebody Helloed me. World them."""
if sender == self . tube . get_unique_name ( ) :
# sender is my bus name,
# so ignore my own signal
return
logger . debug (' Newcomer %s has joined', sender)
logger . debug (
'Welcoming newcomer and sending them '
' the game state ' )
self. other = sender
# I send my ships and I returned theirs
enemy_boats = self . tube . get_obj ect (
self. other, PATH).World(
self .my_boats , dbus_interf ace=IFACE)
# I call the callback World, to load the enemy ships
self . World_cb (enemy_boats)
The most interesting thing about this code is this line, which Python calls a Decorator:
Ssignal (dbus_interf ace=IFACE, signature^' ' )
When you put ©signal in front of a method name it has the effect of adding the two
parameters shown to the method call whenever it is invoked, in effect changing it from
a normal method call to a D-Bus call for a signal. The signature parameter is an empty
string, indicating that the method call has no parameters. The HelloO method does
nothing at all locally but when it is received by the other instances of the Activity it
causes them to execute the WorldO method, which sends back the location of their boats
and gets the new participants boats in return.
Batalla Naval is apparently meant to be a demonstration program. Battleship is a game
for two players, but there is nothing in the code to prevent more players from joining
and no way to handle it if they do. Ideally you would want code to make only the first
joiner an actual player and make the rest only spectators.
Next we'll look at the WorldO method:
# This is called by whoever receives our Hello signal
# This method receives the current game state and
# puts us in sync with the rest of the participants .
# The current game state is represented by the game
# object
Smethod (dbus_interf ace=IFACE, in_signature= ' a (ssiii ) ',
out signature= ' a (ssiii) ')
150
def World(self, boats):
"""To be called on the incoming XO after
they Hello. """
if not self. world:
logger . debug (' Somebody called World on me')
self. world = True # Instead of loading the world,
# I am receiving play by play.
self . World_cb (boats )
# now I can World others
self . add_hello_handler ( )
else :
self. world = True
logger . debug ( "I ' ve already been welcomed, "
"doing nothing")
return self .my_boats
There is another decorator here, this one converting the WorldO method to a D-Bus call
for a method. The signature is more interesting than HelloO had. It means an array of
tuples where each tuple contains two strings and three integers. Each element in the
array represents one ship and its attributes. World_cb is set to point to a method in
BatallaNaval.py, (and so is Play_cb). If you study the initi) code in BatallaNaval.py
you'll see how this happens. WorldO is called in the hello_signal_cb() method we just
looked at. It is sent to the joiner who sent HelloO to us.
Finally we'll look at the PlayO signal:
@ signal (dbus_interf ace=IFACE, signature='ii' )
def Play(self, x, y) :
"""Say Hello to whoever else is in the tube."""
logger . debug (' Running remote play:%s x %s.', x, y)
def add_hello_handler ( self ) :
self. tube. add_signal_receiver (self . play_signal_cb,
'Play', IFACE,
path=PATH, sender_keyword= ' sender ' )
def play_signal_cb ( self , x, y, sender=None) :
"""Somebody placed a stone. """
if sender == self . tube . getunique name ( ) :
return # sender is my bus name, so
# ignore my own signal
logger . debug (' Buddy %s placed a stone at %s x %s',
sender, x, y)
# Call our Play callback
self . Play_cb (x, y)
This is a signal so there is only one signature string, this one indicating that the input
parameters are two integers.
151
There are several ways you could improve this Activity. When playing against the
computer in non-sharing mode the game just makes random moves. The game does
not limit the players to two and make the rest of the joiners spectators. It does not make
the players take turns. When a player succeeds in sinking all the other players ships
nothing happens to mark the event. Finally, gettextO is not used for the text strings
displayed by the Activity so it cannot be translated into languages other than Spanish.
In the tradition of textbooks everywhere I will leave making these improvements as an
exercise for the student.
152
1 6 • Adding Text To Speech
Introduction
Certainly one of the most popular Activities available is Speak, which takes the words
you type in and speaks them out loud, at the same time displaying a cartoon face that
seems to be speaking the words. You might be surprised to learn how little of the code
in that Activity is used to get the words spoken. If your Activity could benefit from
having words spoken out loud (the possibilities for educational Activities and games are
definitely there) this chapter will teach you how to make it happen.
Hello everyone!
We Have Ways To Make You Talk
A couple of ways, actually, and neither one is that painful. They are:
• Running the espeak program directly
• Using the gstreamer espeak plugin
153
Both approaches have their advantages. The first one is the one used by Speak.
(Technically, Speak uses the gstreamer plugin if it is available, and otherwise executes
espeak directly. For what Speak is doing using the gstreamer plugin isn't really
needed). Executing espeak is definitely the simplest method, and may be suitable for
your own Activity. Its big advantage is that you do not need to have the gstreamer
plugin installed. If your Activity needs to run on something other than the latest
version of Sugar this will be something to consider.
The gstreamer plugin is what is used by Read Etexts to do text to speech with
highlighting. For this application we needed to be able to do things that are not possible
by just running espeak. For example:
• We needed to be able to pause and resume speech, because the Activity needs to
speak a whole page worth of text, not just simple phrases.
• We needed to highlight the words being spoken as they are spoken.
You might think that you could achieve these objectives by running espeak on one
word at a time. If you do, don't feel bad because I thought that too. On a fast computer
it sounds really awful, like HAL 9000 developing a stutter towards the end of being
deactivated. On the XO no sounds came out at all.
Originally Read Etexts used speech-dispatcher to do what the gstreamer plugin does.
The developers of that program were very helpful in getting the highlighting in Read
Etexts working, but speech-dispatcher needed to be configured before you could use it
which was an issue for us. (There is more than one kind of text to speech software
available and speech-dispatcher supports most of them. This makes configuration files
inevitable). Aleksey Lim of Sugar Labs came up with the idea of using a gstreamer
plugin and was the one who wrote it. He also rewrote much of Read Etexts so it would
use the plugin if it was available, use speech-dispatcher if not, and would not support
speech if neither was available.
Running espeak Directly
You can run the espeak program from the terminal to try out its options. To see what
options are available for espeak you can use the man command:
man espeak
This will give you a manual page describing how to run the program and what options
are available. The parts of the man page that are most interesting to us are these:
NAME
espeak - A multi-lingual software speech synthesizer.
154
SYNOPSIS
espeak [options] [<words>]
DESCRIPTION
espeak is a software speech synthesizer for English,
and some other languages .
OPTIONS
-p <integer>
Pitch adjustment, to 99, default is 50
-s <integer>
Speed in words per minute, default is 160
-v <voice name>
Use voice file of this name from
espeak- data/ voices
--voices [=<language code>]
Lists the available voices. If =<language code>
is present then only those voices which are
suitable for that language are listed.
Let's try out some of these options. First let's get a list of Voices:
espeak --voices
5
af
5
bs
5
ca
5
cs
5
cy
5
de
5
el
5
en
5
en-
-sc
2
en-
-uk
■nder VoiceName
File
Other
M afrikaans
af
M bosnian
bs
M Catalan
ca
M czech
cs
M welsh-test
cy
M german
de
M greek
el
M default
default
M en-scottish
en/en-sc
(en 4)
M english
en/en
(en 2)
. . . and many more . . .
Now that we know what the names of the voices are we can try them out. How about
English with a French accent?
espeak "Your mother was a hamster and your father \
smelled of elderberries." -v fr
Let's try experimenting with rate and pitch:
espeak "I'm sorry, Dave. I'm afraid I can't \
do that." -s 120 -p 30
The next thing to do is to write some Python code to run espeak. Here is a short
program adapted from the code in Speak:
import re
import subprocess
155
PITCH_MAX = 99
RATE_MAX =99
PITCH_DEFAULT = PITCH_MAX/2
RATE_DEFAULT = RATE_MAX/3
def speak (text, rate=RATE_DEFAULT, pitch=PITCH_DEFAULT,
voice="def ault" ) :
# espeak uses 80 to 370
rate = 80 + (370-80) * int(rate) / 100
subprocess . call ([ "espeak" , "-p", str (pitch),
"-s", str (rate), "-v", voice, text],
stdout=subprocess .PIPE)
def voices ( ) :
out = []
result = subprocess . Popen ([ "espeak" , "--voices"],
stdout=subprocess . PIPE) . communicate ( ) [0]
for line in result . split (' \n ' ) :
m = re .match (
r'\s*\d+\s+([\w-]+)\s+([MF])\s+( [\w_-]+) \s+( .+)
line)
if not m:
continue
language, gender, name, stuff = m. groups ()
if stuff . startswith (' mb/ ' ) or \
name in ( ' en-rhotic ' , ' english_rp ' ,
' english_wmids ' ) :
# these voices don't produce sound
continue
out . append ( (language, name))
return out
def main ( ) :
print voices ( )
speak("I'm afraid I can't do that, Dave.")
speak ("Your mother was a hamster, and your father "
+ "smelled of elderberries!", 30, 60, "fr")
if name == " main " :
main ( )
In the Git repository in the directory Adding TTS this file is named espeak.py. Load
this file into Eric and do Run Script from the Start menu to run it. In addition to
hearing speech you should see this text:
156
K'af, 'afrikaans'), ('bs', 'bosnian'), Cca', ' Catalan'), Ccs', ' Czech'), Ccy', 'welsh-test'), Cde',
'german'), ('el', 'greek'), ('en', 'default'), Cense', 'en-scottish'), ('en-uk', 'english'), Cen-uk-
north', lancashire'), Cen-us', 'english -us'), Cen-wi', 'en-westindies'), Ceo', 'esperanto'),
Ces', 'spanish'), Ces-la', 'spanish-latin-american'), Cfi', 'finnish'), Cfr', 'french'), Cfr-be',
'french'), Cgrc', 'greek-ancient'), Chi', 'hindi-test'), Chr', 'Croatian'), Chu', 'hungarian'),
Chy', 'armenian'), Chy', 'armenian-west'), ('id', 'indonesian-test'), ('is', 'icelandic-test'),
('it', 'Italian'), Cku', 'kurdish'), ('la', 'latin'), Civ', 'latvian'), Cmk', 'macedonian-test'),
CnV, 'dutch-test'), ('no', 'norwegian-test'), ('pi', 'polish'), Cpt', 'brazil'), Cpt-pt',
'portugal'), Cro', 'romanian'), Cm', 'russian_test'), Csk', 'slovak'), Csq', 'albanian'), Csr',
'serbian'), Csv', 'swedish'), Csw', 'swahihi-test'), Cta', 'tamil'), Ctr', 'turkish'), Cvi ',
'vietnam-test'), Czh', 'Mandarin'), ('zh-yue', 'cantonese-test')]
The voicesO function returns a list of voices as one tuple per voice, and eliminates voices
from the list that espeak cannot produce on its own. This list of tuples can be used to
populate a drop down list.
The speakO function adjusts the value of rate so you can input a value between and 99
rather than between 80 to 370. speakO is more complex in the Speak Activity than what
we have here because in that Activity it monitors the spoken audio and generates
mouth movements based on the amplitude of the voice. Making the face move is most
of what the Speak Activity does, and since we aren't doing that we need very little code
to make our Activity speak.
You can use import espeak to include this file in your own Activities.
Using The gstreamer espeak Plugin
The gstreamer espeak plugin can be installed in Fedora 10 or later using Add/Remove
Software.
157
Ada. 1 Remove Software
System Filters Selection Help
bespeak
I*
-•
All packages
Package collections
Newest packages
Ad ml n tools
GNOME desktop
KDE desktop
other desktops
XFCE desktop
Education
Fonts
Gaines
Graphics
n Ll
SoPtwars speech synthesizer (text-to-speech)
e5peak-L40.02-2.fclO (1386)
Development Tiles Tor espeafc
espeaK-cJevei-i,40.D2-z,rciO(i33&)
gnome-speech driver for eSpeak
g n o me - sp e e c h - e s peak -0. 4 .22 - 1. f c 1 [ i 3 B 6 )
:
A simple g streamer plugin to use espeak as a sound
source. It was developed to simplify trie espeak usage In
the Sugar Speak activity The plugin uses given text to
produce audio output.
Project:
Group:
License:
When you have this done you should be able to download the Read Etexts Activity
(the real one, not the simplified version we're using for the book) from ASLO and try
out the Speech tab. You should do that now. It will look something like this:
is only a record of a pic-nic. it has a purpose, which is to suggest to
the reader how he would be likely to see Europe and the East 5] he looked
at them with his own eyes instead of the eyes of those who traveled in
those countries before him. I make small pretense of showing anyone how
he ought to look at objects of interest beyond the sea— other books do
that., and therefore, even if I were competent to do it, there is no need.
I offer no apologies for any departures from the usual style of
travel-writing that may be charged against me— for I think I have seen with
impartial eyes, and I am sure I have written at least honestly, whether
wisely or not.
In this volume I have used portions of letters which I wrote for the
Daily Alta California, of San Francisco, the proprietors of that journal
having waived their rights and given me the necessary permission. I have
also inserted portions of several letters written for the Mew York
Tribune and the Mew York Herald.
THE AUTHOR.
SAN FRANCISCO.
The book used in the earlier screenshots of this manual was Pride and Prejudice by Jane
Austen. To balance things out the rest of the screen shots will be using The Innocents
Abroad by Mark Twain.
158
Gstreamer is a framework for multimedia. If you've watched videos on the web you
are familiar with the concept of streaming media. Instead of downloading a whole song
or a whole movie clip and then playing it, streaming means the downloading and the
playing happen at the same time, with the downloading just a bit behind the playing.
There are many different kinds of media files: MP3's, DivX, WMV, Real Media, and so
on. For every kind of media file Gstreamer has a plugin.
Gstreamer makes use of a concept called pipelining. The idea is that the output of one
program can become the input to another. One way to handle that situation is to put
the output of the first program into a temporary file and have the second program read
it. This would mean that the first program would have to finish running before the
second one could begin. What if you could have both programs run at the same time
and have the second program read the data as the first one wrote it out? It's possible,
and the mechanism for getting data from one program to the other is called a pipe. A
collection of programs joined together in this way is called a pipeline. The program
that feeds data into a pipe is called a source, and the data that takes the data out of the
pipe is called a sink.
The gstreamer espeak plugin uses a simple pipe: text goes into espeak at one end and
sound comes out the other and is sent to your soundcard. You might think that doesn't
sound much different from what we were doing before, but it is. When you just run
espeak the program has to load itself into memory, speak the text you give it into the
sound card, then unload itself. This is one of the reasons you can't just use espeak a
word at a time to achieve speech with highlighted words. There is a short lag while the
program is loading. It isn't that noticeable if you give espeak a complete phrase or
sentence to speak, but if it happens for every word it is very noticeable. Using the
gstreamer plugin we can have espeak loaded into memory all the time, just waiting for
us to send some words into its input pipe. It will speak them and then wait for the next
batch.
Since we can control what goes into the pipe it is possible to pause and resume speech.
The example we'll use here is a version of Read Etexts again, but instead of the
Activity we're going to modify the standalone version. There is nothing special about
the gstreamer plugin that makes it only work with Activities. Any Python program can
use it. I'm only including Text to Speech as a topic in this manual because every Sugar
installation includes espeak and many Activities could find it useful.
There is a in our Git repository named speech.py which looks like this:
import gst
voice = 'default'
159
pitch =
rate = -20
highlight_cb = None
def _create_pipe ( ) :
pipeline = 'espeak name=source ! autoaudiosink '
pipe = gst . parse_launch (pipeline)
def stop_cb(bus, message):
pipe . set_state (gst . STATE_NULL)
def mark_cb (bus, message) :
if message . structure . get_name ( ) == ' espeak-mark '
mark = message . structure [' mark ' ]
highlight_cb (int (mark) )
bus = pipe . get_bus ( )
bus . add_signal_watch ( )
bus. connect (' message: : eos ' , stop_cb)
bus. connect ( 'message: :error' , stop_cb)
bus. connect ( 'message : : element ' , mark_cb)
return (pipe . get_by_name (' source ') , pipe)
def _speech ( source, pipe, words) :
source . props . pitch = pitch
source . props . rate = rate
source . props . voice = voice
source . props . text = words;
pipe. set_state (gst . STATE_PLAYING)
info_source, info_pipe = _create_pipe ( )
play_source, play_pipe = _create_pipe ( )
# track for marks
play source . props . track = 2
def voices ( ) :
return info source .props . voices
def say (words) :
_speech (inf osource, info_pipe, words)
print words
def play (words):
_speech (play_source, play_pipe, words)
def is_stopped ( ) :
for i in play_pipe . get_state ( ) :
if isinstance (i, gst. State) and \
i == gst .STATE_NULL:
return True
return False
def stop ( ) :
play_pipe . set_state (gst . STATE NULL)
160
def is_paused():
for i in play_pipe . get_state ( ) :
if isinstance (i, gst. State) and \
i == gst.STATE_PAUSED:
return True
return False
def pause ( ) :
play_pipe . set_state (gst . STATE_PAUSED)
def rate_up ( ) :
global rate
rate = min(99, rate + 10)
def rate_down() :
global rate
rate = max(-99, rate - 10)
def pitch_up ( ) :
global pitch
pitch = min(99, pitch + 10)
def pitch_down() :
global pitch
pitch = max(-99, pitch - 10)
def prepare_highlighting (label_text ) :
i =
j =
word_begin =
word_end =
current word =
word_tuples = []
omitted = [' ', '\n', u'\r', '_', '[', '{', ']',\
' } ', ' I ', '<', '>', '*', ' + ', '/', 'W ]
omitted_chars = set (omitted)
while i < len (label_text) :
if label_text [i] not in omitted_chars :
word_begin = i
j = i
while j < len (label_text ) and \
label_text [ j ] not in omitted_chars :
word_end = j
i = j
word_t = (word_begin, word_end, \
label_text [word_begin : word_end] . strip ()
if word_t [2] != u'\r' :
word_tuples . append (word_t )
i = i + 1
return word tuples
def add_word_marks (word_tuples) :
"Adds a mark between each word of text."
i =
marked_up_text = '<speak> '
while i < len (word_tuples ) :
word_t = word_tuples [ i]
161
marked_up_text = marked_up_text + \
'<mark name=" ' + str(i) + '"/>' + word_t[2]
i = i + 1
return marked_up_text + '</speak>'
There is another file named ReadEtextsTTS.py which looks like this:
import sys
import os
import zipfile
import pygtk
import gtk
import getopt
import pango
import gobject
import time
import speech
speech_supported = True
try :
import gst
gst . element_f actory_make ( ' e speak ' )
print 'speech supported! '
except Exception, e:
speech_supported = False
print 'speech not supported! '
page=0
PAGE_SIZE =45
class ReadEtextsActivity ( ) :
def init (self) :
"The entry point to the Activity"
speech . highlight_cb = self . highlight_next_word
# print speech . voices ( )
def highlight_next_word (self , word_count) :
if word_count <! len (self . word_tuples ) :
word_tuple = self . word_tuples [word_count ]
textbuffer = self . textview . get_buf f er ( )
tag = textbuffer . createtag ( )
tag. set_property ( 'weight ' , pango . WEIGHT_BOLD)
tag . set_property ( 'foreground', "white")
tag . set_property ( 'background', "black")
iterStart = \
textbuffer . get_iter_at_of f set (word_tuple [ ] )
iterEnd = \
textbuffer . get_iter_at_of f set (word_tuple [ 1 ] )
bounds = textbuf fer . get_bounds ( )
textbuffer. remove_all_tags (bounds [0], bounds [1])
textbuf fer . apply_tag (tag, iterStart, iterEnd)
v_adjustment = \
self . scrolled_window . get_vadjustment ( )
max = v_adjustment . upper - \
v_adjustment .page_size
max = max * word_count
max = max / len (self . word_tuples )
162
v_adjustment . value = max
return True
def keypress_cb ( self , widget, event):
"Respond when the user presses one of the arrow keys"
global done
global speech_supported
keyname = gtk . gdk . keyval_name (event . keyval)
if keyname == 'KP_End' and speech_supported :
if speech . is_paused ( ) or speech . is_stopped () :
speech. play (self. wordsonpage)
else :
speech . pause ( )
return True
if keyname == 'plus' :
self . f ont_increase ( )
return True
if keyname == 'minus' :
self . f on t_de crease ( )
return True
if speech_supported and speech . is_stopped ( ) == False \
and speech . is_paused == False:
# If speech is in progress, ignore other keys.
return True
if keyname == '7':
speech . pitch_down ( )
speech . say (' Pitch Adjusted')
return True
if keyname == '8':
speech . pitch_up ( )
speech . say (' Pitch Adjusted')
return True
if keyname == '9':
speech . rate_down ( )
speech . say (' Rate Adjusted')
return True
if keyname == '0':
speech . rate_up ( )
speech . say (' Rate Adjusted')
return True
if keyname == 'KP_Right' :
self . page_next ( )
return True
if keyname == ' Page_Up ' or keyname == ' KP_Up ' :
self . page_previous ()
return True
if keyname == 'KP_Left' :
self . page_previous ()
return True
if keyname == ' Page_Down ' or keyname == ' KP_Down ' :
self . page_next ( )
return True
if keyname == ' Up ' :
self . scroll_up ( )
return True
if keyname == 'Down' :
self . scroll_down ( )
return True
163
return False
def page_previous (self ) :
global page
page=page-l
if page < 0: page=0
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . upper - \
v_adj ustment .page_size
def page_next ( self ) :
global page
page=page+l
if page >= len (self .page_index) : page=0
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . lower
def f ont_decrease (self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size - 1
if font_size < 1:
font_size = 1
self . f ont_desc . set_size ( f ont_size * 1024)
self .textview . modif y_f ont (self . f ont_desc)
def f ont_increase (self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size + 1
self . f ont_desc . set_size ( f ont_size * 1024)
self. textview .modif y_f ont (self . f ont_desc)
def scroll_down (self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . upper -
v_adj ustment .page_size:
self . page_next ()
return
if v_adj ustment . value < v_adjustment . upper -
v_adj ustment .page_size:
new_value = v_adjustment .value + \
v_adjustment . step_increment
if new_value > v_adjustment . upper - \
v_adjustment .page_size :
new value = v adjustment . upper - \
v_adj ustment .page_size
v adj ustment . value = new value
def scroll_up ( self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . lower :
self . page_previous ()
return
164
if v_adjustment . value > v_adjustment . lower :
new value = v adjustment . value - \
v_adj ustment . step_increment
if new_value < v_adjustment . lower :
new_value = v_adjustment . lower
v adj ustment . value = new value
def show_page ( self , page_number) :
global PAGE_SIZE, current_word
position = self . page_index [pagenumber ]
self . etext_f ile . seek (position)
linecount =
label_text = ' '
textbuffer = self . textview . get_buf f er ( )
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + \
Unicode (line, ' iso-885 9-1 ' )
linecount = linecount + 1
textbuffer. set_text (label_text )
self. textview. set_buf f er (textbuffer)
self . word_tuples = \
speech . prepare_high lighting (label_text )
self . words_on_page = \
speech . add_word_marks (self . word_tuples )
def save_extracted_f ile (self , zipfile, filename) :
"Extract the file to a temp directory for viewing"
filebytes = zipfile . read ( filename)
f = open("/tmp/" + filename, 'w')
try :
f. write (filebytes)
finally:
f . close ( )
def read_f ile ( self , filename) :
"Read the Etext file"
global PAGE_SIZE
if zipfile . is_zipf ile ( filename) :
self.zf = zipfile . ZipFile ( filename, 'r')
self . book_f iles = self . zf . namelist ( )
self . save_extracted_f ile (self.zf, \
self .book_filesTO] )
currentFileName = "/tmp/" + self .book_f iles [ ]
else :
currentFileName = filename
self . etext_f ile = open (currentFileName, "r" )
self . page_index = [ ]
linecount =
while self . etext_f ile :
line = self . etext_f ile . readline ( )
if not line:
break
linecount = linecount + 1
if linecount >= PAGE_SIZE:
position = self . etext_f ile . tell ( )
165
self.page_index. append (position)
linecount =
if filename . endswith (". zip" ) :
os . remove (currentFileName)
def delete_cb ( self , widget, event, data=None):
speech . stop ( )
return False
def destroy_cb (self , widget, data=None):
speech . stop ( )
gtk .main_quit ()
def main (self, file_path) :
self. window = gtk .Window (gtk . WINDOW_TOPLEVEL)
self. window. connect ( "delete_event" , self . delete_cb)
self. window. connect ("destroy", self. destroy_cb)
self . window . set_title ( "Read Etexts Activity")
self . window . set_size_re quest ( 800 , 600 )
self. window. set_border_width (0 )
self . read_f ile ( f ile_path)
self . scrolled_window = gtk . ScrolledWindow (
hadjustment=None, vadjustment=None)
self . textview = gtk . TextView ( )
self.textview. set_edi table (False)
self. textview. set_lef t_margin (50 )
self. textview. set_cursor_visible (False)
self. textview. connect ( "keypressevent" ,
self . keypress_cb)
self . f ont_desc = pango . FontDescription ( "sans 12")
self. textview . modif y_f ont (self . f ont_desc)
self . show_page ( )
self . scrolled_window . add (self. textview)
self. window. add (self . scrolled_window)
self. textview. show()
self . scrolled_window . show ( )
self.window.show()
gtk .main ( )
if name == " main " :
try:
opts, args = getopt . getopt (sys . argv [ 1 : ] , "")
ReadE texts Activity () .main (args [0])
except getopt . error , msg:
print msg
print "This program has no options"
sys . exit (2 )
The program ReadEtextsTTS has only a few changes to make it enabled for speech.
The first one checks for the existence of the gstreamer plugin:
speech_supported = True
try :
import gst
gst . element_f actory_make ( ' e speak ' )
print 'speech supported! '
except Exception, e:
166
speech_supported = False
print 'speech not supported! '
This code detects whether the plugin is installed by attempting to import for the Python
library associated with it named "gst". If the import fails it throws an Exception and we
catch that Exception and use it to set a variable named speech supported to False. We
can check the value of this variable in other places in the program to make a program
that works with Text To Speech if it is available and without it if it is not. Making a
program work in different environments by doing these kinds of checks is called
degrading gracefully. Catching exceptions on imports is a common technique in Python
to achieve this. If you want your Activity to run on older versions of Sugar you may
find yourself using it.
The next bit of code we're going to look at highlights a word in the textview and scrolls
the textview to keep the highlighted word visible.
class ReadEtextsActivity ( ) :
def init (self):
"The entry point to the Activity"
speech . highlight_cb = self . highlight_next_word
# print speech . voices ( )
def highlight_next_word (self , word_count) :
if word_count < len (self . word_tuples ) :
word_tuple = self . word_tuples [word_count ]
textbuffer = self . textview . get_buffer ( )
tag = textbuf f er . create_tag ( )
tag. set_property ( 'weight ' , pango . WEIGHT_BOLD)
tag . set_property ( 'foreground', "white")
tag . set_property ( 'background', "black")
iterStart = \
textbuffer. get_iter_at_of f set (word_tuple [ ] )
iterEnd = \
textbuffer. get_iter_at_of f set (word_tuple [ 1 ] )
bounds = textbuf fer . get_bounds ( )
textbuffer. remove_all_tags (bounds [0], bounds [1])
textbuf fer . apply_tag (tag, iterStart, iterEnd)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
max = v adj ustment . upper - v adjustment .page size
max = max * word_count
max = max / len (self . word_tuples )
v_adj ustment . value = max
return True
In the init method we assign a variable called highlight _cb in speech.py with a
method called highlight_next_word(). This gives speech.py a way to call that method
every time a new word in the textview needs to be highlighted.
167
The next line will print the list of tuples containing Voice names to the terminal if you
uncomment it. We aren't letting the user change voices in this application but it would
not be difficult to add that feature.
The code for the method that highlights the words follows. What it does is look in a list
of tuples that contain the starting and ending offsets of every word in the textarea's text
buffer. The caller of this method passes in a word number (for instance the first word in
the buffer is word 0, the second is word 1, and so on). The method looks up that entry
in the list, gets its starting and ending offsets, removes any previous highlighting, then
highlights the new text. In addition to that it figures out what fraction of the total
number of words the current word is and scrolls the textviewer enough to make sure
that word is visible.
Of course this method works best on pages without many blank lines, which fortunately
is most pages. It does not work so well on title pages. An experienced programmer
could probably come up with a more elegant and reliable way of doing this scrolling.
Let me know what you come up with.
Further down we see the code that gets the keystrokes the user enters and does speech-
related things with them:
def keypress_cb (self , widget, event):
"Respond when the user presses one of the arrow keys"
global done
global speech_supported
keyname = gtk . gdk . keyval_name (event . keyval)
if keyname == 'KP_End' and speech_supported:
if speech . is_paused ( ) or speech . is_stopped () :
speech. play (self. words_on_page)
else :
speech .pause ( )
return True
if speech_supported and speech . is_stopped ( ) == False \
and speech . is_paused == False:
# If speech is in progress, ignore other keys.
return True
if keyname == '7':
speech .pitch_down ( )
speech . say (' Pitch Adjusted')
return True
if keyname == ' 8 ' :
speech .pitch_up ( )
speech . say (' Pitch Adjusted')
return True
if keyname == ' 9 ' :
speech . rate_down ( )
speech . say (' Rate Adjusted')
return True
if keyname == ' ' :
speech . rate_up ( )
speech . say (' Rate Adjusted')
168
return True
As you can see, the functions we're calling are all in the file speech.py that we
imported. You don't have to fully understand how these functions work to make use of
them in your own Activities. Notice that the code as written prevents the user from
changing pitch or rate while speech is in progress. Notice also that there are two
different methods in speech.py for doing speech. playO is the method for doing text to
speech with word highlighting. say() is for saying short phrases produced by the user
interface, in this case "Pitch adjusted" and "Rate adjusted". Of course if you put code
like this in your Activity you would use the _() function so these phrases could be
translated into other languages.
There is one more bit of code we need to do text to speech with highlighting: we need to
prepare the words to be spoken to be highlighted in the textviewer.
def show_page ( self , page_number ) :
global PAGE_SIZE, current_word
position = self . page_index [pagenumber ]
self . etext_f ile . seek (position)
linecount =
label_text = ' '
textbuffer = self . textview . get_buf f er ( )
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + Unicode (line, \
'iso-8859-1' )
linecount = linecount + 1
textbuffer. set_text (label_text )
self. textview. set_buf f er (textbuffer)
self . word_tuples = \
speech . prepare_high lighting (label_text )
self . words_on_page = \
speech . add_word_marks (self . word_tuples )
The beginning of this method reads a page's worth of text into a string called label_text
and puts it into the textview's buffer. The last two lines splits the text into words,
leaving in punctuation, and puts each word and its beginning and ending offsets into a
tuple. The tuples are added to a List.
speech.add_word_marks() converts the words in the List to a document in SSML
(Speech Synthesis Markup Language) format. SSML is a standard for adding tags (sort
of like the tags used to make web pages) to text to tell speech software what to do with
the text. We're just using a very small part of this standard to produce a marked up
document with a mark between each word, like this:
<speak>
<mark name=" " />The<mark name=" 1 " />quick<mark name-"2"/>
brown<mark name="3"/>f ox<mark name="4 "/> jumps
</speak>
169
When espeak reads this file it will do a callback into our program every time it reads one
of the mark tags. The callback will contain the number of the word in the word_tuples
List which it will get from the name attribute of the mark tag. In this way the method
being called will know which word to highlight. The advantage of using the mark name
rather than just highlighting the next word in the textviewer is that if espeak should fail
to do one of the callbacks the highlighting won't be thrown out of sync. This was a
problem with speech-dispatcher.
A callback is just what it sounds like. When one program calls another program it can
pass in a function or method of its own that it wants the second program to call when
something happens.
To try out the new program run
. /ReadEtextsTTS .py bookfile
from the Terminal. You can adjust pitch and rate up and down using the keys 7, 8, 9,
and on the top row of the keyboard. It should say "Pitch Adjusted" or "Rate
Adjusted" when you do that. You can start, pause, and resume speech with
highlighting by using the End key on the keypad. (On the XO laptop the "game" keys
are mapped to what is the numeric keypad on a normal keyboard. This makes these
keys handy for use when the XO is folded into tablet mode and the keyboard is not
available). You cannot change pitch or rate while speech is in progress. Attempts to do
that will be ignored. The program in action looks like this:
170
Alexandria, which will be reached in twenty-four hours. The ruins
of Caesar's Palace, Pompey's Pillar. Cleopatra's Needle, the
Catacombs, and ruins of arc: em Alexandria mill be found worth the
visit. The journey to Cairo, one hundred and thirty miles by rail,
can be made in a lew hours, and from which can be visited the site
of ancient Memphis, Joseph's Granaries, and the Pyramids.
From Alexandria the route will be taken homeward, calling at
Malta, f^!TT?^ {in Sardinia), and Raima {in Majorca), all
magnificent harbors, with charming scenery, and abounding in fruits.
A day or two will be spent at each place, and leaving Parma in the
evening, Valencia in 'jpain will be reached the next morning. A few
days will be spent in this, the finest city of Spain,
From Valencia, the homeward course will be continued, skirting
along the coast of Spain, Alicant, Carthagena, Palos, and Malaga
will be passed but a mile or two dtstant. and Gibraltar reached in
about twenty-four hours.
A stay of one day will bo made here, and the voyage continued to
Madeira, which will be reached in about three days. Captain
Marryatt writes: "I do not know a spot on the globe which so much
astonishes and delights upon first arrival as Madeira." A stay of
one or two days will be made here, which, if time permits, may be
fix-tended, and passing on through the islands, and probably in sight
of the Peak of "leneriffe, a southern track will be taken, and the
Atlantic crossed within the latitudes of the northeast trade winds,
where mild and pleasant weather, and a smooth sea, can always be
expected.
A call will be made at Bermuda, which Sies directly in this route
_
^L
.
That brings us to the end of the topic of Text to Speech. If you're like to see more, the
Git repository for this book has a few more sample programs that use the gstreamer
espeak plugin. These examples were created by the author of the plugin and
demonstrate some other ways you can use it. There's even a "choir" program that
demonstrates multiple voices speaking at the same time.
171
17. Fun With The Journal
Introduction
By default every Activity creates and reads one Journal entry. Most Activities don't
need to do any more with the Journal than that, and if your Activity is like that you
won't need the information in this chapter. Chances are that someday you will want to
do more than that, so if you do keep reading.
First let's review what the Journal is. The Journal is a collection of files that each have
metadata (data about data) associated with them. Metadata is stored as text strings and
includes such things as the Title, Description, Tags, MIME Type, and a screen shot of
the Activity when it was last used.
Your Activity cannot read and write these files directly. Instead Sugar provides an API
(Application Programming Interface) that gives you an indirect way to add, delete and
modify entries in the Journal, as well as a way to search Journal entries and make a list
of entries that meet the search criteria.
The API we'll use is in the datastore package. After version .82 of Sugar this API was
rewritten, so we'll need to learn how to support both versions in the same Activity.
If you've read this far you've seen several examples where Sugar started out doing one
thing and then changed to do the same thing a better way but still provided a way to
create Activities that would work with either the old or the new way. You may be
wondering if it is normal for a project to do this. As a professional programmer I can tell
you that doing tricks like this to maintain backward compatibility is extremely common,
and Sugar does no more of this than any other project. There are decisions made by
Herman Hollerith when he tabulated the 1890 census using punched cards that
computer programmers must live with to this day.
Introducing Sugar Commander
I am a big fan of the concept of the Journal but not so much of the Journal Activity that
Sugar uses to navigate through it and maintain it. My biggest gripe against it is that it
represents the contents of thumb drives and SD cards as if the files on these were also
Journal entries. My feeling is that files and directories are one thing and the Journal is
another, and the user interface should recognize that.
172
Strictly speaking the Journal Activity is and is not an Activity. It inherits code from the
Activity class just like any other Activity, and it is written in Python and uses the same
datastore API that other Activities use. However, it is run in a special way that gives it
powers and abilities far beyond those of an ordinary Activity. In particular it can do two
things:
• It can write to files on external media like thumb drives and SD cards.
• It alone can be used to resume Journal entries using other Activities.
While I would like to write a Journal Activity that does everything the original does but
has a user interface more to my own taste the Sugar security model won't allow that.
Recently I came to the conclusion that a more mild-mannered version of the Journal
Activity might be useful. Just as Kal-El sometimes finds it more useful to be Qark Kent
than Superman, my own Activity might be a worthy alternative to the built-in Journal
Activity when super powers are not needed.
My Activity, which I call Sugar Commander, has two tabs. One represents the Journal
and looks like this:
Journal Files
Ttie_Creatjre5_of_Man by Howard L. Myers
Short stones by an underrated science fiction author
vuho died in the 1970's. Free RTF from the Baen Free
Description Library.
■Howard L. Myers, Science fiction
Title
v MIME
The Angel of the Revolution, by George Griffith
application^
The_creatures_of_Man by Howard L, Myers i.
application/rtf
The Emerald City of Oz. by L Frank Baurn pio
application/zip
The Hound of the Baskervilles, by K conan Doyle P4
application/zip
The Island of Doctor Moreau. by H. C. Wells P?7
application^
The Jupiter weapon, by Charles Louis Fontenay
application/pdf
The Mummy and Miss Nitocris, by George Griffith P8
application^
The Niaht Life of the Gods, bv Thome Smith P7
aDDlcationftn
This tab lets you browse through the Journal sorted by Title or MIME Type, select
entries and view their details, update Title, Description or Tags, and delete entries you
no longer want. The other tab shows files and folders and looks like this:
173
-■' CB M 1D2F-3F2F
Location: sugarcornmander-l.xo
@ Recently Used
£j Desktop
O File System
Q 198 GB Filesystem
Q 515 MB Filesystem
Q30G6 pilesystem
£j Documents
£j Music
^j Pictures
' Videos
Creating Sugar Activities, odt
2| GetlAE.ooks-5.K0
[3 LeerPendnve-l.xo
^ ReadETexts-lS.xu
[3 SugarCommander-l.xo
Q svnz-Ol.xo
Q ViewSlides-ll.xo
12/20/2009
03/15^2010
03/16/2010
03/08/2010
Vesterday at 23:53
15:47
03/13/2010
Copy File to The Journal
This tab lets you browse through the files and folders or the regular file system,
including thumb drives and SD cards. You can select a file and make a Journal entry
out of it by pushing the button at the bottom of the screen.
This Activity has very little code and still manages to do everything an ordinary
Activity can do with the Journal. You can download the Git repository using this
command:
git clone git : //git . sugarlabs . org/sugar-commander/\
mainline . git
There is only one source file, sugarcommander.py:
import logging
import os
import gtk
import pango
import zipfile
from sugar import mime
from sugar . activity import activity
from sugar . datastore import datastore
from sugar . graphics . alert import NotifyAlert
from sugar . graphics import style
from gettext import gettext as _
import gobject
import dbus
COLUMN_TITLE =
COLUMN MIME = 1
174
COLUMN_JOBJECT = 2
DS_DBUS_SERVICE = ' org . laptop . sugar . DataStore '
DS_DBUS_INTERFACE = ' org . laptop . sugar . DataStore '
DS_DBUS_PATH = ' /org/laptop/sugar /DataStore '
_logger = logging . getLogger (' sugar-commander ' )
class SugarCommander (activity .Activity) :
def init (self, handle, create_j ob j ect=True) :
"The entry point to the Activity"
activity .Activity . init (self, handle, False)
self . selected_j ournal_entry = None
self . selected_path = None
canvas = gtk . Notebook ( )
canvas . props . show_border = True
canvas . props . show_tabs = True
canvas . show ( )
self . ls_j ournal = gtk . ListStore (
gob j ect . TYPE_STRING,
gob j ect . TYPE_STRING,
gob j ect . TYPE_PYOB JECT)
self . tv_j ournal = gtk . TreeView (self . ls_j ournal)
self . tv j ournal . set_rules_hint (True)
self. tv_ journal . set_search_column (COLUMN_TITLE)
self . selection_j ournal = \
self . tv_j ournal . get_se lection ( )
self . select ion_j ournal . set mode (
gtk. SELECTION_SINGLE)
self . select ion_j ournal. connect ("changed",
self . select ion_j ournal_cb)
renderer = gtk . CellRendererText ( )
renderer . set_property ( ' wrap-mode ' , gtk . WRAP_WORD)
renderer . set_property ('wrap -width', 500)
renderer . set_property ('width', 500)
self . col_j ournal = gtk . TreeViewColumn (_(' Title ') ,
renderer, text=COLUMN_TITLE)
self. co 1_ journal . set_sort_column_id (COLUMN_TITLE)
self . tv j ournal . append_column ( self. col_j ournal)
mime_renderer = gtk . CellRendererText ( )
mime_renderer . set_property ('width', 500)
self . col_mime = gtk . TreeViewColumn (_(' MIME ') ,
mime_renderer, text=COLUMN_MIME)
self . col_mime . set_sort_column_id (COLUMN_MIME)
self . tv_j ournal . append_column (self . col_mime)
self . list_scroller_j ournal = gtk . ScrolledWindow (
hadjustment=None, vadjustment=None)
self . list_scroller_j ournal . set_policy (
gtk. POLICY_AUTOMATIC, gtk . POLICY_AUTOMATIC )
self . list_scroller_j ournal . add (self . tv j ournal)
label_attributes = pango . AttrList ( )
label__attributes .insert (pango . AttrSize (
14000, 0, -1))
175
label_attributes .insert (pango. At tr Fore ground (
65535, 65535, 65535, 0, -1))
tabl_label = gtk . Label (_( "Journal" ) )
tabl_label . set_at tributes (label_at tributes )
tabl_label . show ( )
self . tv j ournal.show()
self . list_scroller_j ournal.show()
column_table = gtk . Table (rows=l , columns=2,
homogeneous = False)
image_table = gtk . Table (rows=2 , columns=2,
homogeneous=False)
self. image = gtk. Image ()
image_table . attach (self . image, 0, 2, 0, 1,
xoptions=gtk.FILL | gtk. SHRINK,
yoptions=gtk.FILL | gtk. SHRINK,
xpadding=10 ,
ypadding=10 )
self . btn_save = gtk . Button (_( "Save" ) )
self . btn_save .connect ( ' button_press_event ' ,
self . save_button_press_event_cb)
image_table . attach (self .btn_save, 0, 1, 1, 2,
xoptions=gtk. SHRINK,
yoptions=gtk . SHRINK, xpadding=10 ,
ypadding=10 )
self . btn_save .props . sensitive = False
self . btn_save . show ( )
self . btn_delete = gtk. Button (_ ("Delete" ) )
self . btn_delete .connect ( ' button_press_event ' ,
self . delete_button_press_event_cb)
image_table . attach (self .btn_delete, 1, 2, 1, 2,
xoptions=gtk. SHRINK,
yoptions=gtk . SHRINK, xpadding=10 ,
ypadding=10 )
ypadding=iu )
self . btn_delete .props . sensitive = False
self.btn Hplptp.Rhnw/1
self . btn_delete .show
column_table . attach (image_table, 0, 1, 0, 1,
xoptions=gtk.FILL | gtk. SHRINK,
yoptions=gtk . SHRINK, xpadding=10,
ypadding=10 )
entry_table = gtk . Table (rows=3 , columns=2,
homogeneous=False)
title_label = gtk. Label (_( "Title" ) )
entry_table . attach (title_label , 0, 1, 0, 1,
xoptions=gtk. SHRINK,
yoptions=gtk. SHRINK,
xpadding=10, ypadding=10)
title_label . show ( )
self . title_entry = gtk . Entry (max=0 )
entry_table . attach (self . title_entry, 1, 2, 0, 1,
176
xop t ion s=gtk. FILL | gtk. SHRINK,
yoptions=gtk . SHRINK, xpadding=10 , ypadding=10 )
self . title entry . connect ( ' key_press_event ' ,
self . key_press_event_cb)
self . title_entry . show ( )
description_label = gtk . Label (_( "Description" ) )
entry_table . attach (description_label, 0, 1, 1, 2,
xop ti on s=gtk. SHRINK,
yoptions=gtk. SHRINK,
xpadding=10, ypadding=10)
description_label . show ( )
self . description_textview = gtk . TextView ( )
self . description_textview . set_wrap_mode (
gtk.WRAP_WORD)
entry_table .attach (self. description_textview,
1, 2, 1, 2,
xoptions=gtk. EXPAND | gtk. FILL | gtk. SHRINK,
yoptions=gtk. EXPAND | gtk. FILL | gtk. SHRINK,
xpadding=10, ypadding=10)
self . description_textview .props . accepts_tab = False
self . description_textview . connect ( ' key_press_event ' ,
self . key_press_event_cb)
self . description_textview . show ( )
tags_label = gtk . Label (_( "Tags" ) )
entry_table . attach (tags_label, 0, 1, 2, 3,
xoptions=gtk. SHRINK,
yoptions=gtk. SHRINK,
xpadding=10, ypadding=10)
tags_label . show ( )
self . tags_textview = gtk . TextView ( )
self . tags_textview . set_wrap_mode (gtk . WRAP_WORD)
entry_table . attach (self . tagstextview, 1, 2, 2, 3,
xoptions=gtk .FILL,
yop t ion s=gtk. EXPAND | gtk. FILL,
xpadding=10, ypadding=10)
self . tags_textview .props . accepts_tab = False
self . tags_textview .connect ( ' key_press_event ' ,
self . key_press_event_cb)
self . tags_textview . show ( )
entry_table . show ( )
self . scroller_entry = gtk . ScrolledWindow (
hadjustment=None, vadjustment=None)
self. scroller_entry. set_policy (gtk. POLICY_NEVER,
gtk. POLICY_AUTOMATIC)
self . scroller_entry . add_with_viewport (entry_table)
self . scroller_entry . show ( )
column_table .attach (self. scroller_entry,
1, 2, 0, 1,
xop t ion s=gtk. FILL | gt k. EXPAND | gtk. SHRINK,
yop t ion s=gtk. FILL | gt k. EXPAND | gtk. SHRINK,
xpadding=10, ypadding=10)
177
image_table . show ( )
column_table . show ( )
vbox = gtk.VBox (homogeneous=True, spacing=5)
vbox . pack_start (column_table)
vbox . pack_end (self . list_scroller_j ournal)
canvas . append_page (vbox, tabl_label)
self . _f ilechooser = gtk . FileChooserWidget (
action=gtk.FILE_CHOOSER_ACTION_OPEN,
backend=None)
self._filechooser. s e t_curr en t f older ( " /media" )
self . copy_button = gtk.Button(
_("Copy File To The Journal"))
self . copy_button . connect ( ' clicked' ,
self.create_j ournal_entry)
self . copy_button . show ( )
self._filechooser. set_extra_widget (self . copy_button)
preview = gtk. Image ()
self._filechooser. set_preview_widget (preview)
self. _filechooser. connect ( "update -pre view" ,
self .update preview cb, preview)
tab2_label = gtk . Label (_( "Files" ) )
tab2_label . set_at tributes (label_at tributes )
tab2_label . show ( )
canvas . append_page (self._filechooser, tab2_label)
self . set_canvas (canvas)
self . show_all ( )
toolbox = activity . ActivityToolbox (self )
activity_toolbar = toolbox . get_activity_toolbar ( )
activity_toolbar . keep .props .visible = False
activity_toolbar . share .props .visible = False
self . set_toolbox (toolbox)
toolbox . show ( )
self . load_j ournal_table ( )
bus = dbus . SessionBus ( )
remote_ob j ect = bus . get_ob j ect (
DS_DBUS_SERVICE, DS_DBUS_PATH)
_datastore = dbus . Interface (remote_obj ect ,
DS_DBUS_INTERFACE)
_data store . connect_to_signal (' Created ' ,
self . datastore_created_cb)
_data store . connect_to_signal ( 'Updated ' ,
self . datastore_updated_cb)
_data store . connect_to_signal (' Deleted ' ,
self . datastore_deleted_cb)
self . selected_j ournal_entry = None
def update_preview_cb (self , f ile_chooser , preview) :
filename = f ile_chooser . get_preview_f ilename ( )
try :
f ile_mimetype = mime . get_for_file (filename)
178
if f ile_mimetype . startswith (' image/ ' ) :
pixbuf = \
gtk . gdk . pixbuf _new_from_f ile_at_size (
filename,
style . zoom (320 ) , style . zoom (240 ) )
preview. set from pixbuf (pixbuf )
have preview = True
elif f ile_mimetype == ' application/x-cbz ' :
fname = self . extract_image (filename)
pixbuf = \
gtk . gdk . pixbuf _new_f rom_f ile_at_size (
fname,
style . zoom (320 ) , style . zoom (240 ) )
preview. set_f rom_pixbuf (pixbuf)
have preview = True
os . remove ( fname)
else :
havepreview = False
except :
have_preview = False
f ile_chooser . set_preview_widget_active (
have preview)
return
def key_press_event_cb (self , entry, event):
self . btn_save . props . sensitive = True
def save_button_press_event_cb (self , entry, event):
self . update_entry ( )
def delete_button_press_event_cb (self , entry, event):
da tastore. delete (
self . selected_j ournal_entry . ob j ect_id)
def datastore_created_cb (self , uid) :
new_jobject = datastore . get (uid)
iter = self . ls_j ournal . append ( )
title = new_j obj ect .metadata [' title ' ]
self .ls_j ournal. set (iter, COLUMN_TITLE, title)
mime = new_j obj ect .metadata [' mime_type ' ]
self . ls_journal . set (iter, COLUMN_MIME, mime)
self . ls_ journal. set (iter, COLUMN_JOBJECT,
new_j obj ect)
def datastore_updated_cb (self , uid):
new_jobject = datastore . get (uid)
iter = self . ls_j ournal . get_iter_f irst ( )
for row in self . ls_j ournal :
j Obj ect = row[COLUMN_JOBJECT]
if j ob j ect . ob j ect_id == uid:
title = new_j obj ect .metadata [' title ' ]
self . ls_j ournal . set_value (iter,
COLUMN_TITLE, title)
break
iter = self . ls_j ournal . iter_next (iter )
object_id = self . selected_j ournal_entry . obj ect_id
if object_id == uid:
self . set_f orm_f ields (new_j obj ect )
179
def datastore_deleted_cb (self , uid) :
save_path = self . selected_path
iter = self . ls_j ournal . get_iter_f irst ( )
for row in self. Is journal:
jobject = row[COLUMN_JOBJECT]
if j ob j ect . ob j ect_id == uid:
self.ls_journal. remove (iter)
break
iter = self . ls_j ournal . iter_next (iter )
try:
self . select ion_j ournal . select_path (save_path)
self . tv_j ournal . grab_f ocus ( )
except :
self . title_entry . set_text ( ' ' )
description_textbuf f er = \
self . description_textview . get_buf f er ( )
description_textbuf f er . set_text ( ' ' )
tags_textbuf f er = \
self.tags_textview. get_buf f er ( )
tags_textbuf f er . set_text ( ' ' )
self . btn_save .props . sensitive = False
self . btn_delete .props . sensitive = False
self . image . clear ( )
self . image . show ( )
def update_entry (self ) :
needs_update = False
if self . selected_j ournal_entry is None:
return
object_id = self . selected_j ournal_entry . obj ect_id
jobject = datastore . get (ob j ect_id)
old_title = j obj ect .metadata . get (' title ' , None)
if old_title != self . title_entry .props . text :
j obj ect .metadata [' title ' ] = \
self . title_entry .props. text
jobject .metadata [' title_set_by_user ' ] = '1'
needs_update = True
old_tags = j obj ect .metadata . get (' tags ' , None)
new_tags = \
self . tags_textview .props .buffer. props .text
if old_tags != newtags:
j obj ect .metadata [' tags ' ] = new_tags
needs_update = True
old_description = j obj ect .metadata . get (
'description', None)
new_description = \
self . description_textview .props .buffer .props . text
if old_description != new_description :
j obj ect .metadata [' description ' ] = new_description
needs_update = True
180
if needs_update :
da tastore. write (jobject, update_mtime=False,
reply_handler=self . datastore_write_cb,
error_handler=self . datastore_write_error_cb)
self . btn_save . props . sensitive = False
def datastore_write_cb (self ) :
pass
def datastore_write_error_cb (self , error) :
logging. error (
' sugarcommander . datastore_write_error_cb : '
;r ' % error)
I o, i o.
def close (self, skip_save=False) :
"Override the close method so we don't try to
create a Journal entry."
activity .Activity . close (self, True)
def selection_j ournal_cb (self , selection):
self . btn_delete . props . sensitive = True
tv = selection . get_tree_view ( )
model = tv . get_model ( )
sel = selection . get_selected ( )
if sel:
model, iter = sel
jobject = model. getvalue (iter, COLUMN_JOBJECT)
jobject = datastore . get (j obj ect . obj ect_id)
self . selected_j ournal_entry = jobject
self . set_f orm_f ields ( j ob j ect )
self . selected_path = model . get_path (iter )
def set_form_f ields ( self , jobject):
self . title_entry . set_text (jobject.metadata[ 'title' ] )
description_textbuf f er = \
self . description_textview . get_buf f er ( )
if j obj ect .metadata . has_key (' description ' ) :
description_textbuf f er . set_text (
jobject. metadata! 'description' ] )
else :
description_textbuf f er . set_text ( ' ' )
tags_textbuf f er = self . tags_textview . get_buf f er ( )
if j obj ect .metadata . has_key (' tags ' ) :
tags_textbuf f er . set_text (jobject.metadata[ 'tags' ] )
else :
tags_textbuf f er . set_text ( ' ' )
self . create_preview (j obj ect . obj ect_id)
def create_preview ( self , object_id):
jobject = datastore . get (obj ect_id)
if j obj ect .metadata . has_key (' preview ' ) :
preview = j obj ect .metadata [' preview ' ]
if preview is None or preview == ' ' \
or preview == 'None' :
if jobject.metadataf' mime_type ' ] . startswith (
' image/ ' ) :
filename = j ob j ect . get_f ile_path ( )
181
self . show_image (filename)
return
if j object .metadata [ 'mime_type ' ] == \
' application/x-cbz ' :
filename = j ob j ect . get_f ile_path ( )
fname = self . extract_image (filename)
self . show_image (fname)
os . remove ( fname)
return
if j obj ect .metadata . has_key (' preview ' ) and \
len (j obj ect .metadata [' preview '] ) > 4:
if jobject .metadata [ 'preview' ][ 1 : 4 ] == 'PNG':
preview_data = j obj ect . metadata [' preview ' ]
else :
import base64
preview data = \
base 64 . b64 decode (
j obj ect .metadata [ 'preview ' ] )
loader = gtk . gdk . Pixbuf Loader ( )
loader. write (preview_data)
scaled_buf = loader . get_pixbuf ( )
loader . close ( )
self . image . set_f rom_pixbuf (scaled_buf )
self . image . show ( )
else :
self . image . clear ( )
self . image . show ( )
def load_j ournal_table (self ) :
self . btn_save .props . sensitive = False
self . btn_delete .props . sensitive = False
ds_mounts = datastore .mounts ( )
mountpoint_id = None
if len (ds_mounts ) == 1 and \
ds_mounts [0] [ ' id' ] == 1:
pass
else :
for mountpoint in ds_mounts :
id = mountpoint [' id' ]
uri = mountpoint [' uri ' ]
if uri . startswith (' /home ' ) :
mountpoint_id = id
query = { }
if mountpoint_id is not None:
query [ 'mountpoints ' ] = [ mountpoint_id ]
ds_objects, num_objects = \
datastore. find( query, properties=[ ' uid ' ,
'title', ' mime_type ' ] )
self . ls_j ournal. clear ()
for i in xrange (0, num_objects, 1) :
iter = self . ls_j ournal . append ( )
title = ds_obj ects [ i] .metadata [' title ' ]
self .ls_journal. set (iter, COLUMN_TITLE, title)
182
mime = ds_obj ects [ i] .metadata [' mime_type ' ]
self . ls_journal . set (iter, COLUMN_MIME, mime)
self .ls_ journal. set (iter, COLUMN_JOBJECT,
ds_ob j ects [i] )
if not self . selected_j ournal_entry is None and \
self . selected_j ournal_entry . ob j ect_id == \
ds_ob j ects [i] .object_id:
self . select ion_j ournal . select_iter (iter)
self. ls_ journal . set_sort_column_id (COLUMN_TITLE,
gtk. SORT_ASCENDING)
v_adjustment = \
self . list_scroller_j ournal . get_vadjustment ( )
v_adjustment . value =
return ds_ob j ects [ ]
def create_j ournal_entry (self , widget, data=None):
filename = self . _f ilechooser . get_f ilename ( )
j ournal_entry = datastore . create ( )
j ournal_entry. metadata [' title ' ] = \
self . make_new_f ilename ( filename)
j ournal_entry. metadata [' title_set_by_user ' ] = '1'
j ournal_entry . metadata [' keep ' ] = '0'
f ile_mimetype = mime . get_for_file ( filename)
if not f ile_mimetype is None:
j ournal_entry .metadata [' mime_type ' ] = \
f ile^mimetype
j ournal_entry. metadata [' buddies ' ] = ' '
if f ile_mimetype . startswith (' image/ ' ) :
preview = \
self . create_preview_metadata (filename)
elif f ile_mimetype == ' application/x-cbz ' :
fname = self . extractimage ( filename)
preview = self . create_preview_metadata (fname)
os . remove ( fname)
else :
preview = ' '
if not preview == ' ' :
j ournal_entry. metadata [' preview ' ] = \
dbus . ByteArray (preview)
else :
j ournal_entry .metadata [' preview ' ] = ' '
j ournal_entry . f ile_path = filename
datastore . write ( j ournal_entry)
self . alert (_(' Success ') , _('%s added to Journal.')
% self . make_new_f ilename (filename) )
def alert (self, title, text=None) :
alert = Notif yAlert (timeout=20 )
alert . props . title = title
alert . props .msg = text
self . add_alert (alert)
alert. connect ('response', self. alert_cancel_cb)
alert . show ( )
def alert_cancel_cb ( self , alert, response_id) :
self. remove alert (alert)
183
def show_image (self , filename) :
"display a resized image in a preview"
scaled_buf = gtk . gdk .pixbuf_new_f rom_f ile_at_size (
filename,
style . zoom (320 ) , style . zoom (24 ) )
self . image . set_f rompixbuf (scaled_buf )
self . image . show ( )
def extractimage (self , filename) :
zf = zipfile . ZipFile ( filename, 'r')
image^files = zf . namelist ( )
image_f iles . sort ( )
f ile_to_extract = image_f iles [ ]
extract_new_f ilename = self .make_new_f ilename (
f ile_to_extract )
if extract_new_f ilename is None or \
extract_new_f ilename == ' ' :
# skip over directory name if the images
# are in a subdirectory.
f ile_to_extract = image_f iles [ 1 ]
extract_new_f ilename = self . make_new_f ilename (
f ile_to_extract )
if len (image_f iles ) > 0:
if self . save_extracted_f ile ( zf , f ile_to_extract ) :
fname = os .path . j oin (self . get_activity_root () ,
' instance ' ,
extract_new_f ilename)
return fname
def save_extracted_f ile (self , zipfile, filename):
"Extract the file to a temp directory for viewing"
try:
filebytes = zipfile . read (filename)
except zipfile . BadZipf ile, err:
print 'Error opening the zip file: %s' % (err)
return False
except KeyError, err:
self . alert (' Key Error', 'Zipfile key not found: '
+ str ( filename) )
return
outfn = self .make_new_f ilename (filename)
if (outfn == ' ' ) :
return False
fname = os .path . j oin (self . get_activity_root () ,
'instance', outfn)
f = open (fname, 'w')
try:
f. write (filebytes)
finally:
f . close ( )
return True
def make_new_f ilename (self , filename) :
partition_tuple = f ilename . rpartition ('/' )
return partition_tuple [ 2 ]
184
def create_preview_metadata (self , filename) :
f ile_mimetype = mime . get_for_file ( filename)
if not f ile_mimetype . startswith (' image/ ' ) :
return ' '
scaled_pixbuf = \
gtk . gdk . pixbuf_new_f rom_f ile_at_size (
filename,
style. zoom (320) , style. zoom (240) )
preview_data = []
def save_f unc (buf , data) :
data . append (buf)
scaled_pixbuf . save_to_callback (savefunc,
'prig' ,
user_data=preview_data)
preview_data = ' ' . j oin (preview^data)
return preview data
Let's look at this code one method at a time.
Adding A Journal Entry
We add a Journal entry when someone pushes a button on the gtk.FileChooser. This is
the code that gets run:
def create_j ournal_entry (self , widget, data=None):
filename = self . _f ilechooser . get_f ilename ( )
j ournal_entry = datastore . create ( )
j ournal_entry. metadata [' title ' ] = \
self .make_new_f ilename (
filename)
j ournal_entry. metadata [' title_set_by_user ' ] = '!'
j ournal_entry. metadata [' keep ' ] = '0'
f ile_mimetype = mime . get_for_file ( filename)
if not f ile_mimetype is None:
j ournal_entry . metadata [' mime_type ' ] = \
f ile_mimetype
j ournal_entry .metadata [' buddies ' ] = ' '
if file_mimetype . startswith (' image/ ' ) :
preview = self . create_preview_metadata (filename)
elif f ile_mimetype == ' application/x-cbz ' :
fname = self . extractimage ( filename)
preview = self . create_preview_metadata (fname)
os . remove ( fname)
else :
preview = ' '
if not preview == ' ' :
j ournal_entry .metadata [' preview ' ] = \
dbus . ByteArray (preview)
else :
j ournal_entry .metadata [' preview ' ] = ' '
j ournal_entry . f ile_path = filename
185
datastore . write ( j ournal_entry)
The only thing worth commenting on here is the metadata, title is what appears as #3
in the picture below. title_set_by_user is set to 1 so that the Activity won't prompt the
user to change the title when the Activity closes, keep refers to the little star that
appears at the beginning of the Journal entry (see #1 in the picture below). Highlight it
by setting this to 1, otherwise set to 0. buddies is a list of users that collaborated on
the Journal entry, and in this case there aren't any (these show up as #4 in the picture
below).
®
ft
©-
ft
®
OLPCLIbnry
"l-l"
a
£cr**mh4fc
© —
■ a
Screen shot
A
a
Screens hot
fis
-#
\2s
ft
a
friends- view 2
ft
■
: ■ i 1 -' . -V
ft
a
' " > 1 VIEW 1
ft
a
f r i r-n di wicwJ
ft
B
frkn-ds-wifw
ft
#
Drowse Activity
z
® ©
Seconds aq*
9
Seconds a o*
-©
Seconds a>pc
Secpnds D^c-
1^
vy
5 Hours. £5 minutes...
«
r^
-vy
5 '-ljlii 51 rtliiiLil**.
5 hourvM minute*
0'
— ®
■6 h0U""5,-B mirtyt-^S-
O
■6 hours, W mlrpiibes--..
O
preview is an image file in the PNG format that is a screenshot of the Activity in
action. This is created by the Activity itself when it is run so there is no need to make
one when you add a Journal entry. You can simply use an empty string (") for this
property.
Because previews are much more visible in Sugar Commander than they are in the
regular Journal Activity I decided that Sugar Commander should make a preview image
for image files and comic books as soon as they are added to the Journal. To do this I
made a pixbuf of the image that would fit within the scaled dimensions of 320x240
pixels and made a dbus.ByteArray out of it, which is the format that the Journal uses
to store preview images.
186
mime_type describes the format of the file and is generally assigned based on the
filename suffix. For instance, files ending in .html have a MIME type of 'text/html'.
Python has a package called mimetypes that takes a file name and figures out what its
MIME type should be, but Sugar provides its own package to do the same thing. For
most files either one would give the correct answer, but Sugar has its own MIME types
for things like Activity bundles, etc. so for best results you really should use Sugar's
mime package. You can import it like this:
from sugar import mime
The rest of the metadata (icon, modified time) is created automatically.
NOT Adding A Journal Entry
Sugar Activities by default create a Journal entry using the write_file() method. There
will be Activities that don't need to do this. For instance, Get Internet Archive Books
downloads e-books to the Journal, but has no need for a Journal entry of its own. The
same thing is true of Sugar Commander. You might make a game that keeps track of
high scores. You could keep those scores in a Journal entry, but that would require
players to resume the game from the Journal rather than just starting it up from the
Activity Ring. For that reason you might prefer to store the high scores in a file in the
data directory rather than the Journal, and not leave a Journal entry behind at all.
Sugar gives you a way to do that. First you need to specify an extra argument in your
Activity's init () method like this:
class SugarCommander (activity .Activity) :
def init (self, handle, create_j ob j ect=True) :
"The entry point to the Activity"
activity .Activity . init (self, handle, False)
Second, you need to override the close() method like this:
def close (self, skip_save=False) :
"Override the close method so we don't try to
create a Journal entry."
activity .Activity . close (self, True)
That's all there is to it.
Listing Out Journal Entries
If you need to list out Journal entries you can use the findO method of datastore. The
find method takes an argument containing search criteria. If you want to search for
image files you can search by mime-type using a statement like this:
187
ds_objects, num_objects = datastore . f ind (
{ 'mime_type ' : [ ' image/ j peg ' ,
' image /gif ' , ' image/tiff ' , ' image /png ' ] } ,
properties=[ ' uid ' ,
'title', ' mime_type ' ] ) )
You can use any metadata attribute to search on. If you want to list out everything in
the Journal you can use an empty search criteria like this:
ds_objects, num_objects = datastore . find ({} ,
properties= [ 'uid' ,
'title', ' mime_type ' ] ) )
The properties argument specifies what metadata to return for each object in the list.
You should limit these to what you plan to use, but always include uid. One thing you
should never include in a list is preview. This is an image file showing what the
Activity for the Journal object looked like when it was last used. If for some reason you
need this there is a simple way to get it for an individual Journal object, but you never
want to include it in a list because it will slow down your Activity enormously.
Listing out what is in the Journal is complicated because of the datastore rewrite done
for Sugar .84. Before .84 the datastore.findO method listed out both Journal entries and
files on external media like thumb drives and SD cards and you need to figure out
which is which. In .84 and later it only lists out Journal entries. Fortunately it is
possible to write code that supports either behavior. Here is code in Sugar
Commander that only lists Journal entries:
def load_j ournal_table (self ) :
self . btn_save .props . sensitive = False
self . btn_delete .props . sensitive = False
ds_mounts = datastore .mounts ( )
mountpoint_id = None
if len (ds_mounts ) == 1 and ds_mounts[0] ['id'] == 1:
pass
else :
for mountpoint in ds_mounts :
id = mountpoint [' id' ]
uri = mountpoint [' uri ' ]
if uri . startswith (' /home ' ) :
mountpoint_id = id
query = { }
if mountpoint_id is not None:
query [ 'mountpoints ' ] = [ mountpoint^id ]
ds_objects, num_objects = datastore . find (
query, properties= [ ' uid' ,
'title', ' mime_type ' ] )
self . ls_j ournal. clear ()
for i in xrange (0, num_objects, 1) :
iter = self . ls_j ournal . append ( )
title = ds obj ects [ i] .metadata [' title ' ]
188
self . ls_j ournal.set (iter,
COLUMN_TITLE, title)
mime = ds_obj ects [ i] .metadata [' mime_type ' ]
self . ls_journal . set (iter, COLUMN_MIME, mime)
self . ls_journal.se t (iter, COLUMN_JOBJECT,
ds_ob j ects [i] )
if not self . selected_j ournal_entry is None and \
self . selected_j ournal_entry . ob j ect_id == \
ds_objects [i] .object_id:
self . select ion_j ournal . select_iter (iter)
self. ls_ journal . set_sort_column_id (COLUMN_TITLE,
gtk. SORT_ASCENDING)
v_adjustment = \
self . list_scroller_j ournal . get_vadjustment ( )
v_adj ustment . value =
return ds_ob j ects [ ]
We need to use the datastore.mountsO method for two purposes:
• In Sugar .82 and below it will list out all mount points, including the place the
Journal is mounted on and the places external media is mounted on. The
mountpoint is a Python dictionary that contains a uri property (which is the path
to the mount point) and an id property (which is a name given to the mount
point). Every Journal entry has a metadata attribute named mountpoint. The
Journal uri will be the only one starting with /home, so if we limit the search to
Journal objects where the id of that mountpoint equals the mountpoint metadata
in the Journal objects we can easily list only objects from the Journal.
• In Sugar .84 and later the datastore.mountsO method still exists but doesn't tell you
anything about mountpoints. However, you can use the code above to see if there
is only one mountpoint and if its id is 1 . If it is you know you're dealing with the
rewritten datastore of .84 and later. The other difference is that the Journal objects
no longer have metadata with a key of mountpoint. If you use the code above it
will account for this difference and work with either version of Sugar.
What if you want the Sugar .82 behavior, listing both Journal entries and USB files as
Journal objects, in both .82 and .84 and up? I wanted to do that for View Slides and
ended up using this code:
def load_j ournal_table (self ) :
ds_objects, num_objects = datastore . find (
{ 'mime_type ' : [ ' image/ j peg ' ,
'image/gif', 'image/tiff', 'iraage/png' ] } ,
properties= [ ' uid' , 'title', ' mime_type ' ] )
self . ls_right . clear ( )
for i in xrange (0, num_objects, 1) :
iter = self . ls_right . append ( )
title = ds_obj ects [i] .metadata [' title ' ]
mime_type = ds_obj ects [ i] .metadata [' mime_type ' ]
if mime_type == ' image/ jpeg' \
189
and not title . endswith ('. jpg ' ) \
and not title . endswith ('. jpeg ' ) \
and not title . endswith ('. JPG ' ) \
and not title . endswith ('. JPEG 1 ) :
title = title + '.jpg'
if mime_type == 'image/png' \
and not title . endswith ('. png ' ) \
and not title . endswith ('. PNG ') :
title = title + '.png'
if mime_type == 'image/gif' \
and not title . endswith ('. gif ') \
and not title. endswith('. GIF'):
title = title + '.gif
if mime_type == 'image/tiff' \
and not title . endswith ('. tiff ') \
and not title . endswith ('. TIFF ') :
title = title + ' .tiff
self . ls_right. set (iter, COLUMN_IMAGE, title)
j ob j ect_wrapper = Job j ectWrapper ( )
j ob j ect_wrapper . set_j ob j ect (ds_ob j ects [ i ] )
self . ls_right . set (iter, COLUMN_PATH,
j ob j ect_wrapper )
valid_endings = ('.jpg', '.jpeg', '.JPEG',
'.JPG', '.gif, '.GIF', '.tiff,
' .TIFF' , ' .png' , ' .PNG' )
ds_mounts = datastore .mounts ( )
if len (ds_mounts ) == 1 and ds_mounts [ ] ['id'] == 1:
# datastore .mounts ( ) is stubbed out,
# we're running .84 or better
for dirname, dirnames, filenames in os.walk(
' /media ' ) :
if '. olpc . store ' in dirnames:
dirnames . remove ('.olpc. store')
# don't visit .olpc. store directories
for filename in filenames :
if filename . endswith (valid_endings) :
iter = self . ls_right . append ( )
j ob j ect_wrapper = Job j ectWrapper ( )
j ob j ect_wrapper . set_f ile_path (
os .path . j oin (dirname, filename))
self .ls_right . set (iter, COLUMN_IMAGE,
filename)
self . ls_right. set (iter, COLUMN_PATH,
j ob j ect_wrapper )
self . ls_right . set_sort_column_id (COLUMN_IMAGE,
gtk. SORT_ASCENDING)
In this case I use the datastore.mountsO method to figure out what version of the
datastore I have and then if I'm running .84 and later I use os.walkO to create a flat list of
all files in all directories found under the directory /media (which is where USB and SD
cards are always mounted). I can't make these files into directories, but what I can do is
make a wrapper class that can contain either a Journal object or a file and use those
objects where I would normally use Journal objects. The wrapper class looks like this:
190
class JobjectWrapper ( ) :
def init (self):
self. jobject = None
self. file_path = None
def set_j obj ect ( self , jobject):
self. jobject = jobject
def set_f ile_path ( self , file_path) :
self. file_path = file_path
def get_f ile_path ( self ) :
if self. jobject != None:
return self . j ob j ect . get_f ile_path ( )
else :
return self. file_path
Using Journal Entries
When you're ready to read a file stored in a Journal object you can use the
get_file_path() method of the Journal object to get a file path and open it for reading, like
this:
fname = j ob j ect . get_f ile_path ( )
One word of caution: be aware that this path does not exist until you call get_file_pa th ()
and will not exist long after. With the Journal you work with copies of files in the
Journal, not the originals. For that reason you don't want to store the return value of
get_file_path() for later use because later it may not be valid. Instead, store the Journal
object itself and call the method right before you need the path.
Metadata entries for Journal objects generally contain strings and work the way you
would expect, with one exception, which is the preview.
def create_preview ( self , object_id):
jobject = datastore . get (ob j ect_id)
if j obj ect .metadata . has_key (' preview ' ) :
preview = j obj ect .metadata [' preview ' ]
if preview is None or preview == ' ' or
preview == 'None' :
if jobject. metadata!' mime_type ' ] . startswith (
' image/ ' ) :
filename = j ob j ect . get_f ile_path ( )
self . show_image ( filename)
return
if jobject .metadata [ 'mime_type ' ] == \
' application/x-cbz ' :
filename = j ob j ect . get_f ile_path ( )
fname = self . extractimage (filename)
self . show_image ( fname)
os . remove ( fname)
return
191
if j obj ect .metadata . has_key (' preview ' ) and \
len (j obj ect .metadata [' preview '] ) > 4:
if jobject .metadata [ 'preview' ][ 1 : 4 ] == 'PNG':
preview_data = j obj ect . metadata [' preview ' ]
else :
import base64
preview_data = base64 . b64decode (
j obj ect .metadata [ 'preview ' ] )
loader = gtk . gdk . Pixbuf Loader ( )
loader. write (preview_data)
scaled_buf = loader . get_pixbuf ( )
loader . close ( )
self . image . set_f rompixbuf (scaled_buf )
self . image . show ( )
else :
self . image . clear ( )
self . image . show ( )
The preview metadata attribute is different in two ways:
• We should never request preview as metadata to be returned in our list of Journal
objects. We'll need to get a complete copy of the Journal object to get it. Since we
already have a Journal object we can get the complete Journal object by getting its
object id then requesting a new copy from the datastore using the id.
• The preview image is a binary object (dbus.ByteArray) but in versions of Sugar
older than .82 it will be stored as a text string. To accomplish this it is base 64
encoded.
The code you would use to get a complete copy of a Journal object looks like this:
object_id = j ob j ect . ob j ect_id
jobject = datastore . get (obj ect_id)
Now for an explanation of base 64 encoding. You've probably heard that computers
use the base two numbering system, in which the only digits used are 1 and 0. A unit
of data storage that can hold either a zero or a one is called a bit. Computers need to
store information besides numbers, so to accomodate this we group bits into groups of 8
(usually) and these groups are called bytes. If you only use 7 of the 8 bits in a byte you
can store a letter of the Roman alphabet, a punctuation mark, or a single digit, plus
things like tabs and line feed characters. Any file that can be created using only 7 bits
out of the 8 is called a text file. Everything that needs all 8 bits of each byte to make,
including computer programs, movies, music, and pictures of Jessica Alba is a binary.
In versions of Sugar before .82 Journal object metadata can only store text strings.
Somehow we need to represent 8-bit bytes in 7 bits. We do this by grouping the bytes
together into a larger collection of bits and then splitting them back out into groups of 7
bits. Python has the base64 package to do this for us.
192
Base 64 encoding is actually a pretty common technique. If you've ever sent an email
with an attached file the file was base 64 encoded.
The code above has a couple of ways of creating a preview image. If the preview
metadata contains a PNG image it is loaded into a pixbuf and displayed. If there is no
preview metadata but the MIME type is for an image file or a comic book zip file we
create the preview from the Journal entry itself.
The code checks the first three characters of the preview metadata to see if they are
'PNG'. If so, the file is a Portable Network Graphics image stored as a binary and
does not need to be converted from base 64 encoding, otherwise it does.
Updating A Journal Object
The code to update a Journal object looks like this:
def update_entry ( self ) :
needs_update = False
if self . selected_j ournal_entry is None:
return
object_id = self . selected_j ournal_entry . obj ect_id
jobject = datastore . get (ob j ect_id)
old_title = j obj ect .metadata . get (' title ' , None)
if old_title != self . title_entry .props . text :
j obj ect .metadata [' title ' ] = \
self . title_entry .props .text
jobject .metadata [' title_set_by_user ' ] = '1'
needs_update = True
old_tags = j obj ect .metadata . get (' tags ' , None)
new_tags = \
self . tags_textview .props .buffer .props . text
if old_tags != newtags:
j obj ect .metadata [' tags ' ] = new_tags
needs_update = True
old_description = \
jobject. metadata. get ( 'description' , None)
new_description = \
self . description_textview .props. buffer. props. text
if old_description != new_description :
j obj ect .metadata [' description ' ] = \
new_de script ion
needs_update = True
if needs_update :
datastore. write (j obj ect, update_mtime=False,
reply_handler=self . datastore_write_cb,
error handler=self . datastore write error cb)
193
self . btn_save .props . sensitive = False
def datastore_write_cb (self ) :
pass
def datastore_write_error_cb (self , error) :
logging. error (
' sugarcommander . datastore_write_error_cb : '
>r ' % error)
I o, i o.
Deleting A Journal Entry
The code to delete a Journal entry is this:
def delete_button_press_event_cb (self , entry, event):
datastore . delete (
self . select ed_j ournal_entry . ob j ect_id)
Getting Callbacks From The Journal Using D-Bus
In the chapter on Making Shared Activities we saw how D-Bus calls sent over
Telepathy Tubes could be used to send messages from an Activity running on one
computer to the same Activity running on a different computer. D-Bus is not normally
used that way; typically it is used to send messages between programs running on the
same computer.
For example, if you're working with the Journal you can get callbacks whenever the
Journal is updated. You get the callbacks whether the update was done by your
Activity or elsewhere. If it is important for your Activity to know when the Journal has
been updated you'll want to get these callbacks.
The first thing you need to do is define some constants and import the dbus package:
DS_DBUS_SERVICE = ' org . laptop . sugar . DataStore '
DS_DBUS_INTERFACE = ' org . laptop . sugar . DataStore '
DS_DBUS_PATH = ' /org/laptop/sugar /DataStore '
import dbus
Next, in your init () method put code to connect to the signals and do the callbacks:
bus = dbus . SessionBus ( )
remote_ob j ect = bus . get_ob j ect (
DS_DBUS_SERVICE, DS_DBUS_PATH)
_datastore = dbus . Interface (remote_obj ect ,
DS_DBUS_INTERFACE)
_data store . connect_to_signal (' Created ' ,
self . _datastore_created_cb)
_data store . connect_to_signal ( 'Updated ' ,
self . _datastore_updated_cb)
_data store . connect_to_signal (' Deleted ' ,
194
self ._datastore_deleted_cb)
The methods being run by the callbacks might look something like this:
def datastore_created_cb (self , uid) :
new_jobject = datastore . get (uid)
iter = self . ls_j ournal . append ( )
title = new_j obj ect .metadata [' title ' ]
self . ls_j ournal. set (iter,
COLUMN_TITLE, title)
mime = new_j obj ect .metadata [' mime_type ' ]
self . ls_j ournal. set (iter,
COLUMN_MIME, mime)
self . ls_j ournal. set (iter,
COLUMN_JOBJECT, new_jobject)
def datastore_updated_cb (self , uid):
new_jobject = datastore . get (uid)
iter = self . ls_j ournal . get_iter_f irst ( )
for row in self. Is journal:
j Obj ect = row[COLUMN_JOBJECT]
if j ob j ect . ob j ect_id == uid:
title = new_j obj ect .metadata [' title ' ]
self . ls_j ournal . set_value (iter,
COLUMN_TITLE, title)
break
iter = self . ls_j ournal . iter_next (iter )
object_id = \
self . selected_j ournal_entry . obj ect_id
if object_id == uid:
self . set_f orm_f ields (new_j obj ect )
def datastore_deleted_cb (self , uid):
save_path = self . selected_path
iter = self . ls_j ournal . get_iter_f irst ( )
for row in self. Is journal:
j Obj ect = row[COLUMN_JOBJECT]
if j ob j ect . ob j ect_id == uid:
self. ls_j ournal. remove (iter)
break
iter = self . ls_j ournal . iter_next (iter )
try :
self . select ion_j ournal . select_path (
save_path)
self . tv_j ournal . grab_focus ()
except :
self . title_entry . set_text ( ' ' )
description_textbuf f er = \
self . description_textview . get_buf f er ( )
description_textbuf f er . set_text ( ' ' )
tags_textbuf f er = \
self . tags_textview . get_buf f er ( )
tags_textbuf f er . set_text ( ' ' )
self . btn_save . props . sensitive = False
self . btn_delete .props . sensitive = False
self . image . clear ( )
self . image . show ( )
195
The uid passed to each callback method is the object id of the Journal object that has
been added, updated, or deleted. If an entry is added to the Journal I get the Journal
object from the datastore by its uid, then add it to the gtk.ListStore for the
gtk.TreeModel I'm using to list out Journal entries. If an entry is updated or deleted I
need to account for the possibility that the Journal entry I am viewing or editing may
have been updated or removed. I use the uid to figure out which row in the
gtkListStore needs to be removed or modified by looping through the entries in the
gtkListStore looking for a match.
Now you know everything you'll ever need to know to work with the Journal.
196
X O • Making Activities Using PyG
ame
Introduction
PyGame and PyGTK are two different ways to make a Python program with a
graphical user interface. Normally you would not use both in the same program. Each
of them has its own way of creating a window and each has its own way of handling
events.
The base class Activity we have been using is an extension of the PyGTK Window class
and uses PyGTK event handling. The toolbars all Activities use are PyGTK
components. In short, any Activity written in Python must use PyGTK. Putting a
PyGame program in the middle of a PyGTK program is a bit like putting a model ship
in a bottle. Fortunately there is some Python code called SugarGame that will make it
possible to do that.
Before we figure out how we'll get it in the bottle, let's have a look at our ship.
Making A Standalone Game Using PyGame
As you might expect, it's a good idea to make a standalone Python game using PyGame
before you make an Activity out of it. I am not an experienced PyGame developer, but
using the tutorial Rapid Game Development with Python by Richard Jones at this URL:
http://richard.cgpublisher.com/product/pub.84/prod.ll
I was able to put together a modest game in about a day. It would have been sooner
but the tutorial examples had bugs in them and I had to spend a fair amount of time
using The GIMP to create image files for the sprites in the game.
Sprites are small images, often animated, that represent objects in a game. They
generally have a transparent background so they can be drawn on top of a background
image. I used the PNG format for my sprite files because it supports having an alpha
channel (another term that indicates that part of the image is transparent).
PyGame has code to display background images, to create sprites and move them
around on the background, and to detect when sprites collide with one another and do
something when that happens. This is the basis for making a lot of 2D games. There
are lots of games written with PyGame that could be easily adapted to be Sugar
Activities.
197
My game is similar to the car game in the tutorial, but instead of a car I have an
airplane. The airplane is the Demoiselle created by Alberto Santos-Dumont in 1909.
Instead of having "pads" to collide with I have four students of Otto Lilienthal hovering
motionless in their hang gliders. The hang gliders pitch downwards when Santos-
Dumont collides with them. The controls used for the game have been modified too. I
use the Plus and Minus keys on both the main keyboard and the keypad, plus the
keypad 9 and 3 keys, to open and close the throttle and the Up and Down arrows on
both the main keyboard and the keypad to move the joystick forward and back. Using
the keypad keys is useful for a couple of reasons. First, some versions of sugar-
emulator don't recognize the arrow keys on the main keyboard. Second, the arrow
keys on the keypad map to the game controller on the XO laptop, and the non-arrow
keys on the keypad map to the other buttons on the XO laptop screen. These buttons
can be used to play the game when the XO is in tablet mode.
As a flight simulator it isn't much, but it does demonstrate at least some of the things
PyGame can do. Here is the code for the game, which I'm calling Demoiselle:
# ! /usr/bin/env python
import pygame
import math
import sys
class Demoiselle:
"This is a simple demonstration of using PyGame \
sprites and collision detection."
def init (self) :
self . background = pygame . image . load (' sky . jpg ' )
self. screen = pygame . display . get_surf ace ( )
self . screen .blit (self .background, (0, 0))
self. clock = pygame . time . Clock ( )
self. running = True
gliders = [
GliderSprite ( (200, 200)),
GliderSprite ( (800, 200)),
GliderSprite ( (200, 600)),
GliderSprite ( (800, 600)),
]
self. glider_group = pygame . sprite . RenderPlain (
gliders)
def run ( self) :
"This method processes PyGame messages"
rect = self . screen . get_rect ( )
airplane = AirplaneSprite (' demoiselle .png ' ,
rect . center)
airplane_sprite = pygame . sprite . RenderPlain (
airplane)
while self . running :
self. clock. tick (30 )
198
for event in pygame . event . get ( ) :
if event. type == pygame . QUIT :
self. running = False
return
elif event. type == pygame . VTDEORESIZE :
pygame .display . set_mode (event . size,
pygame .RESIZABLE)
self. screen. blit (self. background,
(0,0))
if not hasattr (event , 'key') :
continue
down = event. type == pygame . KEYDOWN
if event. key == pygame . K_DOWN or \
event. key == pygame . K_KP2 :
airplane . j oystick_back = down * 5
elif event. key == pygame. K_UP or \
event. key == pygame . K_KP8 :
airplane . j oystick_f orward = down * -5
elif event. key == pygame . K_EQUALS or \
event. key == pygame . K_KP_PLUS or \
event. key == pygame . K_KP9 :
airplane . throttle_up = down * 2
elif event. key == pygame . K_MINUS or \
event. key == pygame . K_KP_MINUS or \
event. key == pygame . K_KP3 :
airplane . throttle_down = c
down * -2
self . glider_group .clear (self. screen,
self. background)
airplane_sprite .clear (self. screen,
self. background)
collisions = pygame . sprite . spritecollide (
airplane,
self . glider_group, False)
self . glider_group . update (collisions)
self . glider_group .draw (self. screen)
airplane_sprite . update ()
airplane_sprite .draw (self. screen)
pygame .display. flip ()
class AirplaneSprite (pygame . sprite . Sprite) :
"This class represents an airplane, the Demoiselle \
created by Alberto Santos-Dumont"
MAX_FORWARD_SPEED =10
MIN_FORWARD_SPEED = 1
ACCELERATION = 2
TURN_SPEED = 5
def init (self, image, position) :
pygame .sprite. Sprite . init (self)
self . src_image = pygame . image . load (image)
self.rect = pygame. Rect (
self . src_image . get_rect ( ) )
self . position = position
self . rect . center = self .position
self . speed = 1
self . direction =
199
self . j oystick_back = self . j oystick_f orward = \
self . throttle_down = self . throttle_up =
def update (self) :
"This method redraws the airplane in response\
to events . "
self. speed += (self . throttle_up +
self . throttle_down)
if self. speed > self .MAX_FORWARD_SPEED :
self. speed = self .MAX_FORWARD_SPEED
if self. speed < self .MIN_FORWARD_SPEED :
self. speed = self .MIN_FORWARD_SPEED
self . direction += (self . j oystick_f orward + \
self . j oystick_back)
x_coord, y_coord = self .position
rad = self . direction * math. pi / 180
x_coord += -self. speed * math . cos (rad)
y_coord += -self. speed * math . sin (rad)
screen = pygame . display . get_surf ace ( )
if y_coord < :
y_coord = screen . get_height ( )
if x_coord < :
x_coord = screen . get_width ( )
if x_coord > screen . get_width ( ) :
x_coord =
if y_coord > screen . get_height ( ) :
y_coord =
self . position = (x_coord, y_coord)
self. image = pygame . transform. rotate (
self . srcimage, -self . direction)
self.rect = self . image . get_rect ( )
self . rect . center = self .position
class GliderSprite (pygame . sprite . Sprite) :
"This class represents an individual hang \
glider as developed by Otto Lilienthal."
def init (self, position) :
pygame .sprite. Sprite . init (self)
self. normal = pygame . image . load (
' glider_normal . png ' )
self.rect = pygame . Rect (self . normal . get_rect () )
self . rect . center = position
self. image = self. normal
self. hit = pygame . image . load (' glider_hit . png ' )
def update (self, hit_list) :
"This method redraws the glider when it collides\
with the airplane and when it is no longer \
colliding with the airplane."
if self in hit list:
self. image = self. hit
else :
self. image = self. normal
def main ( ) :
"This function is called when the game is run \
200
from the command line"
pygame . init ( )
pygame .display. set mode ( ( ,
game = Demoiselle ()
game . run ( )
sys . exit ( )
pygame .RESIZABLE)
if
main ( )
And here is the game in action:
You'll find the code for this game in the file demoiselle.py in the book examples project
in Git.
Introducing Sugar Game
SugarGame is not part of Sugar proper. If you want to use it you'll need to include the
Python code for SugarGame inside your Activity bundle. I've included the version of
SugarGame I'm using in the book examples project in the sugargame directory, but
when you make your own games you'll want to be sure and get the latest code to
include. You can do that by downloading the project from Gitorious using these
commands:
mkdir sugargame
cd sugargame
git clone git : //git . sugarlabs . org/sugargame/mainline . git
201
You'll see two subdirectories in this project: sugargame and test, plus a README.txt
file that contains information on using sugargame in your own Activities. The test
directory contains a simple PyGame program that can be run either standalone or as an
Activity. The standalone program is in the file named TestGame.py. The Activity,
which is a sort of wrapper around the standalone version, is in file TestActivity.py.
If you run TestGame.py from the command line you'll see it displays a bouncing ball
on a white background. To try running the Activity version you'll need to run
. /setup . py dev
from the command line first. I was not able to get the Activity to work under sugar-
emulator until I made two changes to it:
• I made a copy of the sugargame directory within the test directory.
• I removed the line reading "sys.path.append('..') # Import sugargame package
from top directory." from TestActivity.py. Obviously this line is supposed to
help the program find the sugargame directory in the project but it didn't work in
Fedora 10. Your own experience may be different.
The Activity looks like this:
The PyGame toolbar has a single button that lets you make the bouncing ball pause
and resume bouncing.
202
Making A Sugar Activity Out Of A PyGame Program
Now it's time to put our ship in that bottle. The first thing we need to do is make a
copy of the sugargame directory of the SugarGame project into the mainline directory
of our own project.
The README.txt file in the SugarGame project is worth reading. It tells us to make an
Activity based on the TestActivity.py example in the SugarGame project. This will be
our bottle. Here is the code for mine, which is named DemoiselleActivity.py:
# DemoiselleActivity . py
from gettext import gettext as
import gtk
import pygame
from sugar . activity import activity
from sugar . graphics . toolbutton import ToolButton
import gobject
import sugargame . canvas
import demoiselle2
class DemoiselleActivity (activity .Activity) :
def init (self, handle) :
super (DemoiselleActivity, self) . init (handle)
# Build the activity toolbar.
self . build_toolbar ( )
# Create the game instance.
self. game = demoiselle2 . Demoiselle ( )
# Build the Pygame canvas.
self ._pygamecanvas = \
sugargame .canvas . Pygame Canvas (self)
# Note that set_canvas implicitly calls
# read_file when resuming from the Journal.
self . set_canvas (self . _py game canvas)
self . score = ' '
# Start the game running.
self . _pygame canvas . run_pygame (self . game .run)
def build_toolbar ( self ) :
toolbox = activity . ActivityToolbox (self )
activity_toolbar = toolbox . get_activity_toolbar ( )
activity_toolbar . keep .props . visible = False
activity_toolbar . share .props . visible = False
self . view_toolbar = ViewToolbar ( )
toolbox . add_toolbar (_ ( ' View ' ) , self. view_toolbar )
self . view_toolbar .connect ( 'go-fullscreen' ,
self . view_toolbar_go_f ullscreen_cb)
self. view toolbar . show ( )
203
toolbox . show ( )
self . set_toolbox (toolbox)
def view_toolbar_go_f ullscreen_cb (self , view_toolbar ) :
self. fullscreen ()
def read_f ile ( self , file_path) :
score_file = open ( f ile_path, "r")
while score_file:
self. score = score_f ile . readline ( )
self . game . set_score (int (self. score))
score_f ile .close ( )
def write_f ile (self , file_path) :
score = self . game . getscore ( )
f = open ( f ile_path, ' wb ' )
try :
f. write (str (score) )
finally:
f . close
class ViewToolbar (gtk . Toolbar ) :
gtype_name = 'ViewToolbar'
gsignals = {
'needs-update-size ' : (gob j ect . SIGNAL_RUN_FIRST,
gobject . TYPE_NONE,
([])),
'go-fullscreen' : (gob j ect . SIGNAL_RUN_FIRST,
gobject .TYPE_NONE,
([]))
}
def init (self) :
gtk. Toolbar . init (self)
self . fullscreen = ToolButton (' view-fullscreen ' )
self. fullscreen. set_tooltip (_(' Fullscreen'))
self. fullscreen. connect ( 'clicked' ,
self . f ullscreen_cb)
self . insert (self . fullscreen, -1)
self. fullscreen. show ()
def f ullscreen_cb (self , button):
self . emit ( 'go-fullscreen' )
This is a bit fancier than TestActivity.py. I decided that my game didn't really need to
be paused and resumed, so I replaced the PyGame toolbar with a View toolbar that
lets the user hide the toolbar when it is not needed. I use the read_file() and write_file()
methods to save and restore the game score. (Actually this is faked, because I never put
in any scoring logic in the game). I also hide the Keep and Share controls in the main
toolbar.
As you would expect, getting a ship in a bottle does require the ship to be modified.
Here is demoiselle2.py, which has the modifications:
204
# ! /usr/bin/env python
import pygame
import gtk
import math
import sys
class Demoiselle:
"This is a simple demonstration of using PyGame \
sprites and collision detection."
def init (self):
self. clock = pygame . time . Clock ( )
self. running = True
self . background = pygame . image . load (' sky . jpg ' )
def get_score ( self ) :
return ' 99 '
def run ( self) :
"This method processes PyGame messages"
screen = pygame . display . get_surf ace ( )
screen . blit ( self . background, (0, 0))
gliders = [
GliderSprite ( (200, 200)),
GliderSprite ( (800, 200)),
GliderSprite ( (200, 600)),
GliderSprite ( (800, 600)),
]
glider_group = pygame . sprite . RenderPlain (gliders )
rect = screen . get_rect ( )
airplane = AirplaneSprite (' demoiselle .png ' ,
rect . center)
airplane_sprite = pygame . sprite . RenderPlain (
airplane)
while self . running :
self. clock. tick (30 )
# Pump GTK messages,
while gtk . eventspending ( ) :
gtk . main_iteration ( )
# Pump PyGame messages.
for event in pygame . event . get ( ) :
if event. type == pygame. QUIT:
self. running = False
return
elif event. type == pygame . VTDEORESIZE :
pygame .display . setmode (event . size,
pygame .RESIZABLE)
screen .blit (self .background, (0, 0))
if not hasattr (event , 'key') :
continue
down = event. type == pygame . KEYDOWN
if event. key == pygame. K DOWN or \
205
event . key == pygame . K_KP2 :
airplane . j oystick_back = down * 5
elif event . key == pygame . K_UP or \
event . key == pygame . K_KP 8 :
airplane . j oystick_f orward = down * -5
elif event . key == pygame . K_EQUALS or \
event . key == pygame . K_KP_PLUS or \
event . key == pygame . K_KP9 :
airplane . throttle_up = down * 2
elif event . key == pygame . K_MINUS or \
event. key == pygame . K_KP_MINUS or \
event . key == pygame . K_KP3 :
airplane . throttle_down = down * -2
glider_group .clear (screen, self. background)
airplane_sprite. clear (screen, self. background)
collisions = pygame . sprite . spritecollide (
airplane,
glider_group, False)
glider_group . update (collisions)
glider_group .draw (screen)
airplane_sprite .update ( )
airplane_sprite. draw (screen)
pygame .display . flip ( )
class AirplaneSprite (pygame . sprite . Sprite) :
"This class represents an airplane, the Demoiselle \
created by Alberto Santos-Dumont"
MAX_FORWARD_SPEED =10
MIN_FORWARD_SPEED = 1
ACCELERATION = 2
TURN_SPEED = 5
def init (self, image, position) :
pygame .sprite. Sprite . init (self)
self . src_image = pygame . image . load (image)
self.rect = pygame . Rect (self . src_image . get_rect () )
self . position = position
self . rect . center = self .position
self . speed = 1
self . direction =
self . j oystick_back = self . j oystick_f orward = \
self . throttle_down = self . throttle_up =
def update (self) :
"This method redraws the airplane in response\
to events . "
self. speed += (self . throttle_up +
self . throttle_down)
if self. speed > self .MAX_FORWARD_SPEED :
self. speed = self .MAX_FORWARD_SPEED
if self. speed < self .MIN_FORWARD_SPEED :
self. speed = self .MIN_FORWARD_SPEED
self . direction += (self . j oystick_f orward +
self . j oystick_back)
x_coord, y_coord = self .position
rad = self . direction * math. pi / 180
x_coord += -self. speed * math . cos (rad)
y_coord += -self. speed * math . sin (rad)
206
screen = pygame . display . get_surf ace ( )
if y_coord < :
y_coord = screen . get_height ( )
if x_coord < :
x_coord = screen . get_width ( )
if x_coord > screen . get_width ( ) :
x_coord =
if y_coord > screen . get_height ( ) :
y_coord =
self . position = (x_coord, y_coord)
self. image = pygame . transform. rotate (
self . src_image, -self . direction)
self.rect = self . image . get_rect ( )
self . rect . center = self .position
class GliderSprite (pygame . sprite . Sprite) :
"This class represents an individual hang \
glider as developed by Otto Lilienthal."
def init (self, position) :
pygame .sprite. Sprite . init (self)
self. normal = pygame . image . load (
' glider_normal . png ' )
self.rect = pygame . Rect (self . normal . get_rect () )
self . rect . center = position
self. image = self. normal
self. hit = pygame . image . load (' glider_hit .png ' )
def update (self, hit_list) :
"This method redraws the glider when it collides\
with the airplane and when it is no longer \
colliding with the airplane."
if self in hit_list:
self. image = self. hit
else :
self. image = self. normal
def main ( ) :
"This function is called when the game is run \
from the command line"
pygame . init ( )
pygame . display . set mode (( , 0), pygame . RESIZABLE)
game = Demoiselle ()
game . run ( )
sys . exit ( )
if name == ' main ':
main ( )
Why not load both demoiselle.py and demoiselle2.py in Eric and take a few minutes
to see if you can figure out what changed between the two versions?
Surprisingly little is different. I added some code to the PyGame main loop to check for
PyGTK events and deal with them:
while self . running :
207
self. clock. tick (30 )
# Pump GTK messages .
while gtk . events_pending ( ) :
gtk . main_iteration ( )
# Pump PyGame messages.
for event in pygame . event . get ( ) :
if event. type == pygame. QUIT:
self. running = False
return
elif event. type == pygame . VIDEORESIZE :
pygame .display . set_mode (event . size,
pygame .RESIZABLE)
screen .blit (self . background, (0, 0))
if not hasattr (event , 'key') :
continue
down = event. type == pygame . KEYDOWN
if event . key == pygame . K_DOWN or \
. . . continue dealing with PyGame events . . .
This has the effect of making PyGame and PyGTK take turns handling events. If this
code was not present GTK events would be ignored and you'd have no way to close the
Activity hide the toolbar, etc. You need to add import gtk at the top of the file so these
methods can be found.
Of course I also added the methods to set and return scores:
def get_score (self ) :
return self. score
def set_score (self , score) :
self. score = score
The biggest change is in the init () method of the Demoiselle class. Originally I
had code to display the background image on the screen:
def init (self) :
self . background = pygame . image . load (' sky . jpg ' )
self. screen = pygame . display . get_surf ace ( )
self . screen .blit (self .background, (0, 0))
The problem with this is that sugargame is going to create a special PyGTK Canvas
object to replace the PyGame display and the DemoiselleActivity code hasn't done that
yet, so self.screen will have a value of None. The only way to get around that is to
move any code that refers to the display out of the init () method of the class and
into the beginning of the method that contains the event loop. This may leave you with
an init () method that does little or nothing. About the only thing you'll want there
is code to create instance variables.
208
Nothing we have done to demoiselle2.py will prevent it from being run as a
standalone Python program.
To try out the game run ./setup.py dev from within the
Making_Activities_Using_PyGame directory. When you try out the Activity it
should look like this:
209
-L y • Making New Style Toolbars
Introduction
They say "There's no Toolbar like an old Toolbar" and if your users are not running the
very latest version of Sugar they're right. Activities will need to support the original
style toolbars for some time to come. However, it is possible to make an Activity that
supports both and that is what we'll do in this chapter.
The new style toolbars came about because of problems with the old toolbars. Activity
users were having a hard time figuring out how to quit an Activity because the Close
button is only on the Activity toolbar. If the Activity starts on a different toolbar, as
many do, it is not obvious that you need to switch to the Activity toolbar to quit the
Activity. Another issue brought up was that the Tabs for the toolbars took up screen
real estate that could be better used elsewhere. Let's compare toolbars for similar
Activities. First, the old style toolbar for Read Etexts:
OO
• u
Read b View
Now compare it with the new style toolbar for the Read Activity:
X^OO
This is thinner than the older version and the Close button is always visible. Some
functions are on the main toolbar and others are attached to toolbars that drop down
when you click on their icon. First, the new Activity drop down toolbar:
[ ed Planpt. by Charles Louis Fontenay
Next the Edit toolbar:
210
Finally, the View toolbar:
* * *
Adding New Style Toolbars to Read Etexts II
When working on the original Read Etexts Activity I borrowed a lot of user interface
code from the original Read Activity and I see no reason to stop doing that now. One
complication to doing this is that Read has some dependencies that prevent the latest
version of Read from working with older versions of Sugar, and that being the case
there is no need at all for Read to support both old and new toolbars. Read Etexts IV
will not be so fortunate; it will need to figure out at runtime what kind of toolbar is
supported and use that.
I am able to test the Activity with both old and new style toolbars on the same box
because I'm running Fedora 11, which has an installed Sugar environment that supports
the old toolbars, plus I have downloaded and run sugar-jhbuild, which supports the
new toolbars in its version of Sugar.
Here is the code for ReadEtextsActivity4.py:
import os
import re
import logging
import time
import zipfile
import gtk
import pango
import dbus
import gobject
import telepathy
from sugar . activity import activity
from sugar . graphics . toolbutton import ToolButton
_NEW_TOOLBAR_SUPPORT = True
try :
from sugar . graphics . toolbarbox import ToolbarBox
from sugar . graphics . toolbarbox import ToolbarButton
from sugar . activity . widgets import StopButton
from toolbar import ViewToolbar
from mybutton import MyActivityToolbarButton
except :
_NEW_TOOLBAR_SUPPORT = False
from toolbar import ReadToolbar, ViewToolbar
from sugar . graphics . toggletoolbutton import ToggleToolButton
211
from sugar . graphics .menuitem import Menultem
from sugar . graphics import style
from sugar import network
from sugar . datastore import datastore
from sugar . graphics . alert import NotifyAlert
from gettext import gettext as _
page=0
PAGE_SIZE =45
TOOLBAR_READ = 2
logger = logging . getLogger (' read-etexts2-activity ' )
class ReadHTTPRequestHandler (
network. Chun kedGlibHTTPRe que st Handler) :
"""HTTP Request Handler for transferring document while
collaborating.
RequestHandler class that integrates with Glib mainloop.
It writes the specified file to the client in chunks,
returning control to the mainloop between chunks .
def translate_path (self , path):
"""Return the filepath to the shared document."""
return self . server . filepath
class ReadHTTPServer (network. GlibTCPServer) :
"""HTTP Server for transferring document while
collaborating. """
def init (self, server_address , filepath) :
"""Set up the GlibTCPServer with the
ReadHTTPRequestHandler .
filepath -- path to shared document to be served.
ii ii ii
self . filepath = filepath
network. GlibTCPServer . init (self,
server address,
ReadHTTPRequestHandler)
class ReadURLDownloader (network . GlibURLDownloader) :
" " "URLDownloader that provides content-length
and content-type."""
def get_content_length (self ) :
"""Return the content-length of the download."""
if self._info is not None:
return int (self. _inf o . headers. get (
'Content-Length' ) )
def get_content_type (self ) :
"""Return the content-type of the download."""
if self._info is not None:
return self. _info. headers. get ( 'Content-type')
212
return None
READ_STREAM_SERVICE = ' read-etexts-activity-http '
class ReadEtextsActivity (activity .Activity) :
def init (self, handle) :
"The entry point to the Activity"
global page
activity .Activity . init (self, handle)
self . f ileserver = None
self . ob j ect_id = handle . obj ect_id
if _NEW_TOOLBAR_SUPPORT:
self . create_new_toolbar ( )
else :
self . create_old_toolbar ( )
self . scrolled_window = gtk . ScrolledWindow ( )
self. scrolled_window. set_policy (gtk. POLICY_NEVER,
gtk. POLICY_AUTOMATIC)
self . scrolled_window .props . shadow_type = \
gtk.SHADOW_NONE
self . textview = gtk . TextView ( )
self .textview. set_edi table (False)
self. textview. set_cursor_visible (False)
self. textview. set_lef t_margin (50 )
self. textview. connect ( "keypressevent" ,
self . keypress_cb)
self . progressbar = gtk . ProgressBar ( )
self.progressbar. set_orientation (
gtk.PROGRESS_LEFT_TO_RIGHT)
self. progressbar. set_fraction (0 . 0)
self . scrolled_window . add (self. textview)
self. textview. show ()
self . scrolled_window . show ( )
vbox = gtk.VBox()
vbox . pack_start (self. progressbar, False,
False, 10)
vbox . pack_start ( self . scrolled_window)
self . set_canvas (vbox)
vbox . show ( )
page =
self . clipboard = gtk . Clipboard (
display=gtk . gdk . display_get_def ault () ,
select ion=" CLIPBOARD")
self . textview . grab_focus ()
self . f ont_desc = pango . FontDescription (
"sans %d" % style . zoom ( 10 ) )
self. textview . modif y_f ont (self . f ont_desc)
buffer = self . textview . get_buffer ( )
self .markset id = buff er . connect ( "mark-set" ,
213
self .mark_set_cb)
self . unused_download_tubes = set ()
self . want_document = True
self . download_content_length =
self . download_content_type = None
# Status of temp file used for write_file:
self . tempf ile = None
self . close_requested = False
self. connect ("shared", self. shared_cb)
self . is_received_document = False
if self ._shared_activity and \
handle . obj ect_id == None:
# We're joining, and we don't already have
# the document.
if self . get_shared ( ) :
# Already joined for some reason,
# just get the document
self . j oined_cb (self)
else :
# Wait for a successful join before
# trying to get the document
self. connect ("joined", self.j oined_cb)
def create_old_toolbar (self ) :
toolbox = activity . ActivityToolbox (self )
activity_toolbar = toolbox . get_activity_toolbar ( )
activity_toolbar . keep .props . visible = False
self . edit_toolbar = activity . EditToolbar ( )
self . edit_toolbar . undo .props .visible = False
self . edit_toolbar . redo .props .visible = False
self . edit_toolbar . separator .props . visible = False
self . edit_toolbar . copy . set_sensitive (False)
self . edit_toolbar .copy. connect ( 'clicked' ,
self . edit_toolbar_copy_cb)
self . edit_toolbar .paste .props .visible = False
toolbox . add_toolbar (_ ( ' Edit ' ) , self. edit_toolbar )
self . edit_toolbar . show ( )
self . readtoolbar = ReadToolbar ( )
toolbox . add_toolbar (_ ( ' Read' ) , self. read_toolbar )
self . readtoolbar .back. connect ( 'clicked' ,
self . go_back_cb)
self . readtoolbar . forward. connect ( 'clicked' ,
self . go_f orward_cb)
self . read_toolbar . num_page_entry .connect ( 'activate' ,
self . num_page_entry_activate_cb)
self . readtoolbar . show ( )
self . view_toolbar = ViewToolbar ( )
toolbox . add_toolbar (_ ( ' View ' ) , self. view_toolbar )
self . view_toolbar .connect ( 'go-fullscreen' ,
self . view_toolbar_go_f ullscreen_cb)
self . view_toolbar . zoom_in .connect ( 'clicked' ,
self. zoom in cb)
214
self . view_toolbar . zoom_out .connect ( 'clicked' ,
self . zoom_out_cb)
self . view_toolbar . show ( )
self . set_toolbox (toolbox)
toolbox . show ( )
self. toolbox. set_current_toolbar (TOOLBAR_READ)
def create_new_toolbar (self ) :
toolbar_box = ToolbarBox ( )
activity_button = MyActivityToolbarButton (self )
toolbar_box .toolbar. insert (activity_button, )
activity_button . show ( )
self . edit_toolbar = activity . EditToolbar ( )
self . edit_toolbar . undo .props . visible = False
self . edit_toolbar . redo .props . visible = False
self . edit_toolbar . separator .props . visible = False
self . edit_toolbar . copy . set_sensitive (False)
self . edit_toolbar .copy. connect ( 'clicked' ,
self . edit_toolbar_copy_cb)
self . edit_toolbar . paste .props . visible = False
edit_toolbar_button = ToolbarButton (
page=self. edit_toolbar ,
icon_name= ' toolbar-edit ' )
self . edit_toolbar . show ( )
toolbar_box .toolbar. insert (edit_toolbar_button, -1 )
edit_toolbar_button . show ( )
self . view_toolbar = ViewToolbar ( )
self . view_toolbar .connect ( 'go-fullscreen' ,
self . view_toolbar_go_f ullscreen_cb)
self . view_toolbar . zoom_in .connect ( 'clicked' ,
self . zoom_in_cb)
self . view_toolbar . zoom_out .connect ( 'clicked' ,
self . zoom_out_cb)
self . view_toolbar . show ( )
view_toolbar_button = ToolbarButton (
page=self. view_toolbar ,
icon_name= ' toolbar-view ' )
toolbar_box .toolbar. insert (view_toolbar_button, -1 )
view_toolbar_button . show ( )
self. back = ToolButton (' go-previous ' )
self. back. set_tooltip (_ ( ' Back ' ) )
self . back . props . sensitive = False
self. back. connect ( 'clicked' , self. go_back_cb)
toolbar_box .toolbar. insert (self. back, -1)
self. back. show ( )
self. forward = ToolButton (' go-next ' )
self. forward. set_tooltip (_( 'Forward'))
self . forward. props . sensitive = False
self. forward. connect ( 'clicked' ,
self . go_f orward_cb)
toolbar box . toolbar . insert (self . forward, -1)
215
self. forward. show ()
num_page_item = gtk. Toolltem ( )
self . num_page_entry = gtk.EntryO
self . num_page_entry . set_text ( ' ' )
self . num_page_entry . set_alignment ( 1 )
self . num_page_entry .connect ( ' insert-text' ,
self . new_num_page_entry_insert_text_cb)
self . num_page_entry .connect ( 'activate' ,
self . new_num_page_entry_activate_cb)
self . num_page_entry . set_width_chars (4 )
num_page_item. add (self . num_page_entry)
self . num_page_entry . show ( )
toolbar_box .toolbar. insert (num_page_item, -1 )
num_page_item. show ( )
total_page_item = gtk . Toolltem ( )
self . total_page_label = gtk. Label ()
label_attributes = pango . AttrList ( )
label_attributes .insert (pango . AttrSize (
14000, 0, -1))
label_attributes .insert (pango. At tr Foreground (
65535, 65535, 65535, 0, -1))
self . total_page_label . set_at tributes (
label_attributes)
self . total_page_label . set_text ( ' / 0')
total_page_item. add (self . total_page_label )
self . total_page_label . show ( )
toolbar_box .toolbar. insert (total_page_item, -1 )
total_page_item. show ( )
separator = gtk . SeparatorToolItem ( )
separator . props . draw = False
separator. setexpand (True)
toolbar_box .toolbar. insert (separator, -1)
separator. show ()
stop_button = StopButton (self )
stop_button .props . accelerator = ' <Ctrl><Shif t>Q '
toolbar_box .toolbar. insert (stop_button, -1 )
stop_button . show ( )
self . set_toolbar_box (toolbar_box)
toolbar_box . show ( )
def new_num_page_entry_insert_text_cb (self , entry,
text, length, position) :
if not re. match (' [0-9] ' , text):
entry. emit_stop_by_name ('insert-text')
return True
return False
def new_num_page_entry_activate_cb (self , entry) :
global page
if entry . props . text :
new_page = int (entry .props . text ) - 1
216
else :
new_page =
if new_page >= self . total_pages :
new_page = self . total_pages - 1
elif new_page < 0:
new_page =
self . current_page = newpage
self . set_current_page (new_page)
self . show_page (new_page)
entry. props . text = str (newpage + 1)
self . update_nav_buttons ()
page = new_page
clef update_nav_buttons (self ) :
current page = self. current page
self . back . props . sensitive = current_page >
self . forward. props . sensitive = \
current_page < self . total_pages - 1
self . num_page_entry . props . text = str (
current_page + 1)
self . total_page_label .props . label = \
' / ' + str ( self . total_pages)
clef set_total_pages ( self , pages):
self . total_pages = pages
def set_current_page ( self , page):
self . current_page = page
self . update_nav_buttons ()
def keypress_cb ( self , widget, event):
"Respond when the user presses one of the \
arrow keys"
keyname = gtk . gdk . keyval_name (event . keyval)
print keyname
if keyname == 'plus' :
self . f ont_increase ( )
return True
if keyname == 'minus' :
self . f on t_de crease ( )
return True
if keyname == ' Page_Up ' :
self . page_previous ()
return True
if keyname == ' Page_Down ' :
self . page_next ( )
return True
if keyname == 'Up' or keyname == ' KP_Up ' \
or keyname == 'KP_Left' :
self . scroll_up ( )
return True
if keyname == 'Down' or keyname == ' KP_Down ' \
or keyname == 'KP_Right' :
self . scroll_down ( )
return True
217
return False
def num_page_entry_activate_cb (self , entry) :
global page
if entry . props . text :
new_page = int (entry .props . text ) - 1
else :
new_page =
if new_page >= self . read_toolbar . total_pages :
new_page = self . readtoolbar . total_pages - 1
elif new_page < 0:
new_page =
self . readtoolbar . current_page = newpage
self . readtoolbar . set_current_page (new_page)
self . show_page (new_page)
entry . props . text = str (newpage + 1)
self . readtoolbar . update_nav_buttons ( )
page = newpage
def go_back_cb (self , button) :
self . page_previous ()
def go_f orward_cb (self , button):
self . page_next ()
def page_previous (self ) :
global page
page=page-l
if page < 0: page=0
if _NEW_TOOLBAR_SUPPORT:
self . set_current_page (page)
else :
self . readtoolbar . set_current_page (page)
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . upper - \
v_adj ustment .page_size
def page_next ( self ) :
global page
page=page+l
if page >= len (self .page_index) : page=0
if _NEW_TOOLBAR_SUPPORT:
self . set_current_page (page)
else :
self . read_toolbar . set_current_page (page)
self . show_page (page)
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
v_adj ustment . value = v_adjustment . lower
def zoom_in_cb (self , button) :
self . f ont_increase ( )
def zoom_out_cb (self , button) :
218
self . f on t_de crease ( )
def f ont_decrease ( self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size - 1
if font_size < 1:
font_size = 1
self . f ont_desc . set_size ( f ont_size * 1024)
self .textview . modif y_f ont (self . f ont_desc)
def f ont_increase ( self ) :
font_size = self . f ont_desc . get_size ( ) / 1024
font_size = font_size + 1
self . f ont_desc . set_size ( f ontsize * 1024)
self. textview .modif y_f ont (self . f ont_desc)
def raark_set_cb (self, textbuffer, iter, textmark) :
if textbuf f er . get_has_selection ( ) :
begin, end = textbuf fer . get_selection_bounds ( )
self . edit_toolbar . copy . set_sensitive (True)
else :
self . edit_toolbar . copy . set_sensitive (False)
def edit_toolbar_copy_cb (self , button):
textbuffer = self . textview . get_buf fer ( )
begin, end = textbuf fer . get_selection_bounds ( )
copy_text = textbuf fer . get_text (begin, end)
self. clipboard. set_text (copy_text )
def view_toolbar_go_f ullscreen_cb (self , view_toolbar ) :
self. fullscreen ()
def scrolldown ( self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . upper -
v_adj ustment .page_size:
self . page_next ( )
return
if v_adj ustment . value < v_adjustment . upper - \
v_adj ustment .page_size:
new_value = v_adjustment . value + \
v_adj ustment . step_increment
if new_value > v_adjustment . upper - \
v_adj ustment .page_size:
new value = v adjustment . upper - \
v_adj ustment .page_size
v adj ustment . value = new value
\
def scroll_up ( self ) :
v_adj ustment = \
self . scrolled_window . get_vadj ustment ( )
if v_adj ustment . value == v_adjustment . lower :
self . page_previous ()
return
if v_adj ustment . value > v_adjustment . lower :
new value = v adjustment . value - \
219
v_adjustment . step_increment
if new_value < v_adjustment . lower :
new_value = v_adjustment . lower
v adj ustment . value = new value
def show_page ( self , pagenumber ) :
global PAGE_SIZE, current_word
position = self .page_index [page_number ]
self . etext_f ile .seek (position)
linecount =
label_text = '\n\n\n'
textbuffer = self . textview . get_buffer ( )
while linecount < PAGE_SIZE:
line = self . etext_f ile . readline ( )
label_text = label_text + Unicode (line,
'iso-8859-1' )
linecount = linecount + 1
label_text = label_text + '\n\n\n'
textbuffer. set_text (label_text )
self. textview. setbuffer (textbuffer)
def save_extracted_f ile (self , zipfile, filename):
"Extract the file to a temp directory for viewing"
filebytes = zipfile . read ( filename)
outfn = self .make_new_f ilename (filename)
if (outfn == ' ' ) :
return False
f = open (os .path . j oin (self . get_activity_root () ,
' tmp ' , outfn) , ' w ' )
try :
f. write (filebytes)
finally:
f . close ( )
def get_saved_page_number (self ) :
global page
title = self .metadata . get (' title ' , '')
if title == ' ' or not title [ len (title) -1 ]. isdigit () :
page =
else :
i = len(title) - 1
newPage = ' '
while (titlefi] .isdigit() and i > 0) :
newPage = title [i] + newPage
i = i - 1
if title [i] == ' P' :
page = int (newPage) - 1
else :
# not a page number; maybe a volume number.
page =
def save_page_number (self ) :
global page
title = self .metadata . get (' title ' , '')
if title == ' ' or not title [ len (title) -1 ]. isdigit ( )
title = title + ' P' + str(page + 1)
else :
i = len(title) - 1
220
while (title [i] .isdigit() and i > 0) :
i = i - 1
if title [i] == ' P' :
title = title[0:i] + 'P' + str (page + 1)
else :
title = title + ' P' + str (page + 1)
self .metadata [' title ' ] = title
def read_f ile ( self , filename) :
"Read the Etext file"
global PAGE_SIZE, page
tempfile = os . path . j oin (self . get_activity_root () ,
'instance', 'tmp%i' % time . time () )
os . link ( filename, tempfile)
self . tempfile = tempfile
if zipf ile . is_zipf ile ( filename) :
self.zf = zipf ile . ZipFile ( filename, 'r')
self . book_f iles = self . zf . namelist ( )
self . save_extracted_f ile (self . zf ,
self .book_filesT0] )
currentFileName = os .path . j oin (
self . get_activity_root () ,
' tmp ' , self .book_f iles [ ] )
else :
currentFileName = filename
self . etext_f ile = open (currentFileName, "r" )
self . page_index = [ ]
pagecount =
linecount =
while self . etext_f ile :
line = self . etext_f ile . readline ( )
if not line:
break
linecount = linecount + 1
if linecount >= PAGE_SIZE:
position = self . etext_f ile . tell ( )
self . page_index . append (position)
linecount =
pagecount = pagecount + 1
if filename . endswith (". zip" ) :
os . remove (currentFileName)
self . get_saved_page_number ( )
self . show_page (page)
if _NEW_TOOLBAR_SUPPORT:
self . set_total_pages (pagecount + 1)
self . set_current_page (page)
else :
self . read_toolbar . set_total_pages (
pagecount + 1)
self . read_toolbar . set_current_page (page)
# We've got the document, so if we're a shared
# activity, offer it
if self . get_shared ( ) :
self. watch for tubes ()
221
self . sharedocument ( )
def make_new_f ilename (self , filename) :
partition_tuple = f ilename . rpartition ('/' )
return partition_tuple [ 2 ]
def write_f ile (self , filename) :
"Save meta data for the file."
if self . is_received_document :
# This document was given to us by someone,
# so we have to save it to the Journal.
self.etext_file.seek(0)
filebytes = self . etext_f ile . read ( )
print ' saving shared document '
f = open ( filename, ' wb ' )
try :
f .write (filebytes)
finally:
f . close ( )
elif self . tempf ile :
if self . close_requested:
os . link (self . tempf ile, filename)
logger . debug (
"Removing temp file %s because "
"we will close",
self . tempf ile)
os.unlink(self. tempf ile)
self . tempf ile = None
else :
# skip saving empty file
raise NotlmplementedError
self .metadata [' activity ' ] = self . get_bundle_id ( )
self . save_page_number ( )
def can_close ( self ) :
self . close_requested = True
return True
def j oined_cb ( self , also_self ) :
"""Callback for when a shared activity is joined.
Get the shared document from another participant.
ii ii ii
self . watch_f or_tubes ()
gob j ect . idle_add (self . get_document )
def get_document (self ) :
if not self . want_document :
return False
# Assign a file path to download if one
# doesn't exist yet
if not self . _j obj ect . file_path :
path = os .path . j oin (self . get_activity_root () ,
'instance', 'tmp%i' % time . time () )
else :
path = self . _j obj ect . file_path
222
# Pick an arbitrary tube we can try to download
# the document from
try :
tube_id = self . unused_download_tubes .pop ( )
except (ValueError, KeyError) , e:
logger . debug (
'No tubes to get the document from '
'right now: %s ' , e)
return False
# Avoid trying to download the document
# multiple times at once
self . want_document = False
gobj ect . idle_add ( self . download_document ,
tube_id, path)
return False
def download_document ( self , tube_id, path):
chan = self ._shared_activity . telepathy_tubes_chan
iface = chan [telepathy. CHANNEL_TYPE_TUBES]
addr = if ace . AcceptStreamTube (tube_id,
telepathy. S0CKET_ADDRESS_TYPE_IPV4 ,
telepathy. SOCKET_ACCESS_CONTROL_LOCALHOST,
0,
utf8_strings=True)
logger . debug (
'Accepted stream tube: '
'listening address is %r',
addr)
assert isinstance (addr, dbus. Struct)
assert len (addr) == 2
assert isinstance (addr [ ] , str)
assert isinstance (addr [ 1 ] , (int, long))
assert addr[l] > and addr[l] < 65536
port = int (addr [1 ] )
self.progressbar. show ()
getter = ReadURLDownloader (
"http : //%s : %d/ document "
% (addr [0] , port) )
getter. connect ("finished",
self . download_result_cb, tube_id)
getter. connect ("progress",
self . download_progress_cb, tube_id)
getter. connect ("error",
self . download_error_cb, tube_id)
logger . debug ( "Starting download to %s...", path)
getter. start (path)
self . download_content_length = \
getter . get_content_length ( )
self . download_content_type = \
getter . get_content_type ( )
return False
def download_progress_cb (self , getter,
bytes_downloaded, tube_id) :
if self . download_content_length > 0:
223
logger . debug (
"Downloaded %u of %u bytes from tube %u.
bytes_downloaded,
self . download_content_length,
tube_id)
else :
logger . debug (
"Downloaded %u bytes from tube %u...",
bytes_downloaded, tube_id)
total = self . download_content_length
self . set_downloaded_bytes (bytes_downloaded,
total)
gtk . gdk . threads_enter ( )
while gtk . eventspending ( ) :
gtk .main_iteration ( )
gtk.gdk.threads_leave ()
def set_downloaded_bytes (self , bytes, total):
fraction = float (bytes) / float (total)
self .progressbar.set_fraction (fraction)
logger . debug ( "Downloaded percent", fraction)
def clear_downloaded_bytes (self ) :
self.progressbar.set_fraction(0.0)
logger . debug ( "Cleared download bytes")
def download_error_cb (self , getter, err, tube_id) :
self . progressbar . hide ()
logger . debug (
"Error getting document from tube %u: %s",
tube_id, err)
self. alert (_(' Failure') ,
_( 'Error getting document from tube'))
self . want_document = True
self . download_content_length =
self . download_content_type = None
gob j ect . idle_add (self . get_document )
def download_result_cb (self , getter, tempfile,
suggested_name, tube_id) :
if self . download_content_type . startswith (
'text/html ' ) :
# got an error page instead
self . download_error_cb (getter,
'HTTP Error', tube_id)
return
del self . unused_download_tubes
self . tempfile = tempfile
file_path = os .path . j oin (
self . get_activity_root () ,
'instance', ' %i ' % time . time () )
logger . debug (
"Saving file %s to datastore. . .", file_path)
os . link (tempfile, file_path)
self . j obj ect . file_path = file_path
datastore . write (self . _j obj ect ,
224
transf er_ownership=True)
logger . debug ( "Got document %s (%s) from tube %u",
tempfile, suggested name, tube_id)
self . is_received_document = True
self . read_f ile (tempfile)
self . save ( )
self . progressbar . hide ()
def shared_cb ( self , activityid) :
"""Callback when activity shared.
Set up to share the document.
# We initiated this activity and have now
# shared it, so by definition we have the file,
logger . debug ( 'Activity became shared')
self . watch_f or_tubes ()
self . share_document ( )
def share_document ( self ) :
"""Share the document."""
h = hash ( self ._activity_id)
port = 1024 + Th % 64511)
logger . debug (
'Starting HTTP server on port %d', port)
self . f ileserver = ReadHTTPServer ( ( " " , port),
self . tempfile)
# Make a tube for it
chan = self ._shared_activity . telepathy_tubes_chan
iface = chan [telepathy. CHANNEL_TYPE_TUBES]
self . f ileserver_tube_id = if ace . Of f erStreamTube (
READ_STREAM_SERVICE,
U,
telepathy. SOCKET_ADDRESS_TYPE_IPV4 ,
('127.0.0.1', dbus.UIntl6 (port) ) ,
telepathy . SOCKET_ACCESS_CONTROL_LOCALHOST, 0)
def watch_f or_tubes ( self ) :
"""Watch for new tubes."""
tubes_chan = \
self . _shared_activity . telepathy_tubes_chan
tubes_chan [telepathy . CHANNEL_TYPE_TUBES] . \
connect_to_signal (
'NewTube ' ,
self . new_tube_cb)
tubes_chan [telepathy . CHANNEL_TYPE_TUBES] .ListTubes (
reply_handler=self . list_tubes_reply_cb,
error_handler=self . list_tubes_error_cb)
def new_tube_cb ( self , tube_id, initiator, tube_type,
service, params, state) :
"""Callback when a new tube becomes available."""
logger . debug (
'New tube: ID=%d initator=%d type=%d service=%s
225
'params=%r state=%d', tube_id,
initiator, tube_type,
service, params, state)
if service == READ_STREAM_SERVICE :
logger . debug (' I could download from that tube')
self . unused_download_tubes . add (tube_id)
# if no download is in progress, let's
# fetch the document
if self . want_document :
gob j ect . idle_add (self . get_document )
def list_tubes_reply_cb (self , tubes):
"""Callback when new tubes are available."""
for tube_info in tubes:
self . new_tube_cb ( *tube_inf o)
def list_tubes_error_cb (self , e):
"""Handle ListTubes error by logging."""
logger . error (' ListTubes ( ) failed: %s ' , e)
def alert (self, title, text=None) :
alert = Notif yAlert (timeout=20 )
alert . props . title = title
alert . props .msg = text
self . add_alert (alert)
alert. connect ( 'response', self. alert_cancel_cb)
alert . show ( )
def alert_cancel_cb (self , alert, response_id) :
self . remove_alert (alert)
self . textview . grab_f ocus ()
226
Here is what it looks like running under sugar-jhbuild:
the most of it."
Mr. Wickham's society mas of material service in dispelling the gloom
which trie late perverse occurrences had thrown on many of the Longbourn
family They saw him often, and to his other recommendations was now
added that of general unreserve, The whole of what Elizabeth had already
heard, his claims on Mr. Darcy, and all that he had suffered from him,
was now openly acknowledged and publicly canvassed; and everybody was
pleased to know how much they had always disliked Mr Darcy before they
had known anything of the matter.
Miss Bennet was the only creature who could suppose there might be
any extenuating circumstances in the case, unknown to the society
of Hertfordshire; her mild and steady candour always pleaded for
allowances, and urged the possibility of mistakes--but by everybody else
Mr. Darcy was condemned as the worst of men.
Let's have a look at how it works. ! If you've paid attention to other chapters when I've
talked about the idea of "degrading gracefully" the imports in this code will be about
what you would expect:
_NEW_TOOLBAR_SUPPORT = True
try :
from sugar . graphics . toolbarbox import ToolbarBox
from sugar . graphics . toolbarbox import ToolbarButton
from sugar . activity . widgets import StopButton
from toolbar import ViewToolbar
from mybutton import MyActivityToolbarButton
except :
_NEW_TOOLBAR_SUPPORT = False
from toolbar import ReadToolbar , ViewToolbar
Here we try to import a bunch of stuff that only exists in versions of Sugar that support
the new toolbars. If we succeed, then _NEW_TOOLBAR_SUPPORT will remain set to
True. If any of the imports fail then the variable is set to False. Note that a couple of
imports that should always succeed are placed after the three that might fail. If any of
the first three fail I don't want these imports to be done.
This next bit of code in the init () method should not be surprising:
if _NEW_TOOLBAR_SUPPORT:
self . createnewtoolbar ( )
else :
227
self . create_old_toolbar ( )
I moved creating the toolbars into their own methods to make it easier to compare how
the two different toolbars are created. The old toolbar code is unchanged. Here is the
new toolbar code:
def create newtoolbar (self ) :
toolbar_box = ToolbarBox ( )
activity_button = MyActivityToolbarButton (self )
toolbar_box .toolbar. insert (activity_button, )
activity_button . show ( )
self . edit_toolbar = activity . EditToolbar ( )
self . edit_toolbar . undo .props .visible = False
self . edit_toolbar . redo .props .visible = False
self . edit_toolbar . separator .props . visible = False
self . edit_toolbar . copy . set_sensitive (False)
self . edit_toolbar .copy. connect ( 'clicked' ,
self . edit_toolbar_copy_cb)
self . edit_toolbar .paste .props .visible = False
edit_toolbar_button = ToolbarButton (
page=self. edit_toolbar ,
icon_name= ' toolbar-edit ' )
self . edit_toolbar . show ( )
toolbar_box .toolbar. insert (edit_toolbar_button, -1 )
edit_toolbar_button . show ( )
self . view_toolbar = ViewToolbar ( )
self . view_toolbar .connect ( 'go-fullscreen' ,
self . view_toolbar_go_f ullscreen_cb)
self . view_toolbar . zoom_in .connect ( 'clicked' ,
self . zoom_in_cb)
self . view_toolbar . zoom_out .connect ( 'clicked' ,
self . zoom_out_cb)
self . view_toolbar . show ( )
view_toolbar_button = ToolbarButton (
page=self. viewtoolbar,
icon_name= 'toolbar-view' )
toolbar_box .toolbar. insert (
view_toolbar_button, -1)
view_toolbar_button . show ( )
self. back = ToolButton (' go-previous ' )
self. back. set_tooltip (_ ( ' Back ' ) )
self . back . props . sensitive = False
self. back. connect ( 'clicked' , self. go_back_cb)
toolbar_box .toolbar. insert (self. back, -1)
self . back . show ( )
self. forward = ToolButton (' go-next ' )
self. forward. set_tooltip (_( 'Forward'))
self . forward. props . sensitive = False
self. forward. connect ( 'clicked' ,
self . go_f orward_cb)
toolbar_box .toolbar. insert (self. forward, -1)
228
self. forward. show ()
num_page_item = gtk . Toolltem ( )
self . nura_page_entry = gtk. Entry ()
self . num_page_entry . set_text ( ' ' )
self . num_page_entry . set_alignment ( 1 )
self . num_page_entry .connect ( ' insert-text' ,
self . new_num_page_entry_insert_text_cb)
self . num_page_entry .connect ( 'activate' ,
self . new_num_page_entry_activate_cb)
self . num_page_entry . set_width_chars (4 )
num_page_item. add ( self . num_page_entry)
self . num_page_entry . show ( )
toolbar_box .toolbar. insert (num_page_item, -1 )
num_page_item . show ( )
total_page_item = gtk . Toolltem ( )
self . total_page_label = gtk. Label ()
label_attributes = pango . AttrList ( )
label_attributes .insert (pango . AttrSize (
14000, 0, -1))
label_attributes .insert (pango . At tr Foreground (
65535, 65535, 65535, 0, -1))
self . total_page_label . set_at tributes (
label_attributes)
self . total_page_label . set_text ( ' / 0')
total_page_item. add (self . total_page_label )
self . total_page_label . show ( )
toolbar_box .toolbar. insert (total_page_item, -1 )
total_page_item. show ( )
separator = gtk . SeparatorToolItem ( )
separator . props . draw = False
separator. set_expand (True)
toolbar_box .toolbar. insert (separator, -1)
separator . show ( )
stop_button = StopButton (self )
stop_button . props . accelerator = ' <Ctrl><Shif t>Q '
toolbar_box .toolbar. insert (stop_button, -1 )
stop_button . show ( )
self . set_toolbar_box (toolbar_box)
toolbar_box . show ( )
def new_num_page_entry_insert_text_cb (self , entry,
text, length, position) :
if not re .match ('[ 0-9] ' , text):
entry. emit_stop_by_name ('insert-text')
return True
return False
def new_num_page_entry_activate_cb (self , entry) :
global page
if entry . props . text :
new_page = int (entry .props . text ) - 1
229
else :
new_page =
if new_page >= self . total_pages :
new_page = self . total_pages - 1
elif new_page < 0:
new_page =
self . current_page = newpage
self . set_current_page (new_page)
self . show_page (new_page)
entry . props . text = str (newpage + 1)
self . update navbuttons ( )
page = newpage
def update_nav_buttons (self ) :
current page = self. current page
self . back . props . sensitive = current_page >
self . forward. props . sensitive = \
current_page < self . total_pages - 1
self . num_page_entry .props . text = str (
current_page + 1)
self . total_page_label .props . label = \
' / ' + str (self . total_pages )
def set_total_pages (self , pages):
self . total_pages = pages
def set_current_page (self , page):
self . current_page = page
self . update_nav_buttons ( )
Much of the code in the two methods is the same. In particular, the View toolbar and
the Edit toolbar are exactly the same in both. Instead of becoming the active toolbar
they drop down from the toolbar to become sub toolbars. If we had done the Read
toolbar the same way we could have implemented both old and new toolbars with very
little code. However, the Read toolbar contains controls that are important enough to
the Activity that they should be available at all times, so we put them in the main
toolbar instead. Because of this every place where the code refers to the Read toolbar
has to have two ways it can be performed, like this:
if _NEW_TOOLBAR_SUPPORT:
self . set_total_pages (pagecount + 1)
self . set_current_page (page)
else :
self . read_toolbar . set_total_pages (
pagecount + 1)
self . readtoolbar . set_current_page (page)
There is one more point of interest when it comes to the main toolbar. When you have
an old style toolbar you get the stop button as part of the Activity toolbar. With the
new style toolbar you need to add it to the end of the main toolbar yourself:
230
separator = gtk . SeparatorToolItem ( )
separator . props . draw = False
separator . set_expand (True)
toolbar_box .toolbar. insert (separator, -1)
separator . show ( )
stop_button = StopButton (self )
stop_button . props . accelerator = ' <Ctrl><Shif t>Q '
toolbar_box .toolbar. insert (stop_button, -1 )
stop_button . show ( )
Note that you must put a gtk.SeparatorToolItem with set_expand() equal to True
before the StopButton. This will push the button all the way to the right of the toolbar,
where it belongs.
That just leaves the Activity toolbar to discuss:
toolbar_box = ToolbarBox ( )
activity_button = MyActivityToolbarButton (self )
toolbar_box .toolbar. insert (activity_button, )
activity_button . show ( )
Normally you would use the class ActivityToolbarButton to create the default drop
down Activity toolbar. The problem I have with that is if I do that there is no way to
hide the Keep button or the Share control. This version of the Activity needs the
Share control, but has no use at all for the Keep button.
There have been some spirited discussions about the Keep button on the mailing lists.
New computer users don't know what it's for, and experienced computer users expect it
to be like a Save Game button or a Save As... menu option in a regular application. It
isn't quite like either one, and that can lead to confusion. For these reasons I've decided
that no Activity of mine will leave the Keep button unhidden. To hide the button I
copied a bit of the code for the original ActivityToolbarButton in a file named
mybutton.py:
import gtk
import gconf
from sugar . graphics . toolbarbox import ToolbarButton
from sugar . activity . widgets import ActivityToolbar
from sugar . graphics . xocolor import XoColor
from sugar . graphics . icon import Icon
from sugar . bundle . activitybundle import ActivityBundle
def _create_activity_icon (metadata) :
if metadata . get (' icon-color ' , ''):
color = XoColor (metadata [' icon-color '] )
else :
client = gconf . client_get_def ault ( )
color = XoColor (client . get_string (
'/desktop/sugar/user/color'))
231
from sugar . activity . activity import get_bundle_path
bundle = ActivityBundle (get_bundle_path ( ) )
icon = Icon ( f ile=bundle . get_icon ( ) , xo_color=color )
return icon
class MyActivityToolbarButton (ToolbarButton) :
def init (self, activity, **kwargs) :
toolbar = ActivityToolbar (activity,
orientation_lef t=True)
toolbar. stop. hide ()
toolbar . keep . hide ( )
ToolbarButton . init (self, page=toolbar,
**kwargs)
icon = _create_activity_icon (activity . metadata)
self . set_icon_widget (icon)
icon . show ( )
The line in bold is the one difference between mine and the original. If toolbar had
been made an instance variable (self.toolbar) I could have used the original class.
232
APPENDIX
20. Where To Go From Here?
21. About The Authors
22. License
233
ZCK Where To Go From Here?
This book attempts to give a beginning programmer the information she needs to
develop and publish her own Sugar Activities. It already contains many URL's of
websites containing information not covered in the book. This chapter will contain
URL's and pointers to still more resources that will be useful to any Sugar developer.
PyGTK Book by Peter Gill
Much of the work you will do writing Activities involves PyGTK. Peter Gill is working
on a PyGTK book that covers the subject in great detail. You can download the book
here:
http://www.majorsilence.com/PyGTK B ook
OLPC Austria Activity Handbook
This is the first attempt to write a manual on creating Sugar Activities. It is aimed at
experienced programmers and covers topics that this book does not, like how to write
Activities using languages other than Python. The book was written in 2008 and as a
result some of the advice is a bit dated. It's still an excellent source of information. The
authors are Christoph Derndorfer and Daniel Jahre.
http://wiki.sugarlabs.Org/images/5/51/Activity Handbook 200805 online.pdf
http : //www, olpcaustria. org
The Sugar Almanac
This is a series of Wiki articles covering the Sugar API (Application Programming
Interface). It's a good source of information that I have referred to many times.
http://wiki.sugarlabs.org/go/Development Team/Almanac
234
Sugar Labs Mailing Lists
Sugar Labs has several email mailing lists that might be worth subscribing to. The ones
I follow most are the IAEP (It's An Education Proiject) list and Sugar-Devel. Sugar-
Devel is a good place to ask questions about developing Sugar Activities and learn
about the latest work being done on Sugar itself. IAEP is a good place to get ideas on
what kinds of Activities teachers and students want and to get feedback on your own
Activities. Anyone can sign up to these mailing lists here:
http://lists.sugarlabs.org/
PyDoc
PyDoc is a utility for viewing documentation generated from the Python libraries on
your computer, including the Sugar libraries. To run it use this command from a
terminal:
pydoc -p 1234
This command will not finish. It runs a kind of web server on your system where 1234
is a port number. You can access the website it serves at http://localhost:1234. There
is nothing magic about the number 1234. You can use any number you like.
The website lets you follow links to documentation on all the Python libraries you have
installed. When you are done browsing the documentation you can stop the pydoc
command bt returning to the terminal and hitting Ctrl-C (hold down the Ctrl key and hit
the "c" key).
235
Lk A • About The Authors
James Simmons
James Simmons has programmed professionally since 1978. Back then computer
programs were made using a special machine that punched holes into cards, reels of
tape were the most common data storage medium, and hard disks were so expensive
and exotic that the hard disk inventory of a Fortune 500 company would today be
considered barely large enough to hold a nice picture of Jessica Alba.
The industry has come a long way since then, and to a lesser extent so has James.
James learned to program at Oakton Community College in Morton Grove, Illinois and
Western Illinois University in Macomb, Illinois. Times were hard back then and a
young man's best chance of being employed after graduation was to become an
Accountant or a Computer Programmer. It was while he attended OCC that James saw
a Monty Python sketch about an Accountant who wished to become a Lion Tamer.
This convinced James that he should become a Computer Programmer.
James' studies at WIU got off to a rough start when he signed up for Basic Assembly
Language as his first real computer class, erroneously thinking that the word "Basic"
meant "for beginners". From the computer's point of view it was basic, but for students
not so much. He barely passed the course with a "D" but in the process learned that he
enjoyed programming computers. He decided to continue his computer studies and
graduated with a Bachelor's Degree in Information Science.
James was born in 1956, the year before Sputnik went up. He was a nerdy kid. At
various times he fooled around with Erector sets, chemistry sets, microscopes,
dissecting kits, model cars, model planes, model rockets, amateur radio, film making,
and writing science fiction stories. He achieved no real success with any of these
activities.
James participated in the first Give One Get One promotion of the One Laptop Per Child
project and started developing Activities for the Sugar platform soon after. He has
written the Activities Read Etexts, View Slides, Sugar Commander and Get
Internet Archive Books.
236
James Cameron
James Cameron has programmed as a child since 1978, and professionally since 1982.
He learned on programmable calculators, Apple II, TRS-80, Commodore 64, and then
DEC VAX.
James completed a Bachelor's Degree in Business in 1991, majoring in Management
Information Systems. He has worked for electrical engineering and computer
manufacturing companies. He became interested in One Laptop Per Child as a
volunteer and provided radio range testing in the Australian outback, and is now
working for OLPC as System Test Coordinator.
James reviewed the example code in this book and made many suggestions for
improving it.
Oceana Rain Fields
Oceana Rain Fields - Oceana is a visual artist and creative spirit with a flair for the
unexpected and the desire to support worthy causes with her art. She graduated in 2010
from Pacific High School, earning several notable scholarships. In 2010, her painting
"Malaria" won first in show in the Vision 2010 high school art competition at the Coos
Art Museum in Coos B ay, Oregon. Oceana plans to continue her art education at
Southwestern Oregon Community College in Fall 2010.
Oceana is responsible for the cover art of this book. As a "mentee" of the Rural Design
Collective, she also did cover and interior illustrations for another FLOSS Manual: An E-
Book Revolution: Reading and Leading with One Laptop Per Child.
237
22.
License
All chapters copyright of the authors (see below). Unless otherwise stated all chapters in
this manual licensed with GNU General Public License version 2
This documentation is free documentation; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later version.
This documentation is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along with this
documentation; if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
Fifth Floor, Boston, MA 02110-1301, USA.
Acknowledgements
Many people contributed to this book besides the authors listed. They offered advice,
technical support, corrections, and much code. If I tried to list all of their names I might
leave someone out, so let me just thank all the members of the Sugar-Devel mailing list.
Cover art Copyright (Q 2010 by Oceana Rain Fields.
Authors
ABOUT THE AUTHORS
© James Simmons 2010
MAKING ACTIVITIES USING PYGAME
© James Simmons 2010
ADD REFINEMENTS
© James Simmons 2009, 2010
Modifications:
Lachlan Musicman 2010
CREATING YOUR FIRST ACTIVITY
238
© Anne Gentle 2009
Modifications:
James Simmons 2009, 2010
Lachlan Musicman 2010
CREDITS
© adam hyde 2006, 2007
Modifications:
James Simmons 2010
Lachlan Musicman 2010
DISTRIBUTE YOUR ACTIVITY
© James Simmons 2010
Modifications:
Lachlan Musicman 2010
FUN WITH THE JOURNAL
© James Simmons 2010
GOING INTERNATIONAL WITH FOOTLE
© James Simmons 2010
Modifications:
Lachlan Musicman 2010
INHERIT FROM SUGAR. ACTIVITY. ACTIVITY
© James Simmons 2009, 2010
Modifications:
Lachlan Musicman 2010
INTRODUCTION
© adam hyde 2006, 2007
Modifications:
James Simmons 2009, 2010
Lachlan Musicman 2010
WHERE TO GO FROM HERE?
© James Simmons 2010
WHAT DO I NEED TO KNOW TO WRITE A SUGAR ACTIVITY?
© Anne Gentle 2009
Modifications:
James Simmons 2009, 2010
239
Lachlan Musicman 2010
MAKING NEW STYLE TOOLBARS
© James Simmons 2010
PACKAGE THE ACTIVITY
© James Simmons 2009, 2010
Modifications:
Lachlan Musicman 2010
SETTING UP A DEVELOPMENT ENVIRONMENT
© Anne Gentle 2009
Modifications:
James Simmons 2009, 2010
Lachlan Musicman 2010
A STANDALONE PYTHON PROGRAM FOR READING ETEXTS
© James Simmons 2009, 2010
Modifications:
Lachlan Musicman 2010
MAKING SHARED ACTIVITIES
© James Simmons 2010
Modifications:
TWikiGuest 2010
DEBUGGING SUGAR ACTIVITIES
© James Simmons 2010
ADDING TEXT TO SPEECH
© James Simmons 2010
Modifications:
Lachlan Musicman 2010
ADD YOUR ACTIVITY CODE TO VERSION CONTROL
© James Simmons 2010
Modifications:
Lachlan Musicman 2010
WHAT IS SUGAR?
© Anne Gentle 2009
Modifications:
James Simmons 2009, 2010
240
Lachlan Musicman 2010
WHAT IS A SUGAR ACTIVITY?
© Anne Gentle 2009
Modifications:
James Simmons 2009, 2010
Lachlan Musicman 2010
□
FLOSS
MANUALS
Free manuals for free software
General Public License
Version 2, June 1991
Copyright (Q 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share and
change it. By contrast, the GNU General Public License is intended to guarantee your
freedom to share and change free software—to make sure the software is free for all its
users. This General Public License applies to most of the Free Software Foundation's
software and to any other program whose authors commit to using it. (Some other Free
Software Foundation software is covered by the GNU Lesser General Public License
instead.) You can apply it to your programs, too.
241
When we speak of free software, we are referring to freedom, not price. Our General
Public Licenses are designed to make sure that you have the freedom to distribute
copies of free software (and charge for this service if you wish), that you receive source
code or can get it if you want it, that you can change the software or use pieces of it in
new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to deny you
these rights or to ask you to surrender the rights. These restrictions translate to certain
responsibilities for you if you distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for a fee, you
must give the recipients all the rights that you have. You must make sure that they, too,
receive or can get the source code. And you must show them these terms so they know
their rights.
We protect your rights with two steps: (1) copyright the software, and (2) offer you this
license which gives you legal permission to copy, distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain that everyone
understands that there is no warranty for this free software. If the software is modified
by someone else and passed on, we want its recipients to know that what they have is
not the original, so that any problems introduced by others will not reflect on the
original authors' reputations.
Finally, any free program is threatened constantly by software patents. We wish to
avoid the danger that redistributors of a free program will individually obtain patent
licenses, in effect making the program proprietary. To prevent this, we have made it
clear that any patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
MODIFICATION
0. This License applies to any program or other work which contains a notice placed by
the copyright holder saying it may be distributed under the terms of this General Public
License. The "Program", below, refers to any such program or work, and a "work based
on the Program" means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it, either verbatim or with
modifications and/or translated into another language. (Hereinafter, translation is
included without limitation in the term "modification".) Each licensee is addressed as
"you".
242
Activities other than copying, distribution and modification are not covered by this
License; they are outside its scope. The act of running the Program is not restricted, and
the output from the Program is covered only if its contents constitute a work based on
the Program (independent of having been made by running the Program). Whether
that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and appropriately publish
on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all
the notices that refer to this License and to the absence of any warranty; and give any
other recipients of the Program a copy of this License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may at your
option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it, thus
forming a work based on the Program, and copy and distribute such modifications or
work under the terms of Section 1 above, provided that you also meet all of these
conditions:
a) You must cause the modified files to carry prominent notices stating that you
changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or in part
contains or is derived from the Program or any part thereof, to be licensed as a
whole at no charge to all third parties under the terms of this License.
c) If the modified program normally reads commands interactively when run, you
must cause it, when started running for such interactive use in the most ordinary
way, to print or display an announcement including an appropriate copyright
notice and a notice that there is no warranty (or else, saying that you provide a
warranty) and that users may redistribute the program under these conditions, and
telling the user how to view a copy of this License. (Exception: if the Program itself
is interactive but does not normally print such an announcement, your work based
on the Program is not required to print an announcement.)
243
These requirements apply to the modified work as a whole. If identifiable sections of
that work are not derived from the Program, and can be reasonably considered
independent and separate works in themselves, then this License, and its terms, do not
apply to those sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based on the Program,
the distribution of the whole must be on the terms of this License, whose permissions
for other licensees extend to the entire whole, and thus to each and every part regardless
of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work
written entirely by you; rather, the intent is to exercise the right to control the
distribution of derivative or collective works based on the Program.
In addition, mere aggregation of another work not based on the Program with the
Program (or with a work based on the Program) on a volume of a storage or distribution
medium does not bring the other work under the scope of this License.
3. You may copy and distribute the Program (or a work based on it, under Section 2) in
object code or executable form under the terms of Sections 1 and 2 above provided that
you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code,
which must be distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give any third
party, for a charge no more than your cost of physically performing source
distribution, a complete machine-readable copy of the corresponding source code,
to be distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute
corresponding source code. (This alternative is allowed only for noncommercial
distribution and only if you received the program in object code or executable form
with such an offer, in accord with Subsection b above.)
244
The source code for a work means the preferred form of the work for making
modifications to it. For an executable work, complete source code means all the source
code for all modules it contains, plus any associated interface definition files, plus the
scripts used to control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include anything that is normally
distributed (in either source or binary form) with the major components (compiler,
kernel, and so on) of the operating system on which the executable runs, unless that
component itself accompanies the executable.
If distribution of executable or object code is made by offering access to copy from a
designated place, then offering equivalent access to copy the source code from the same
place counts as distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program except as expressly
provided under this License. Any attempt otherwise to copy, modify, sublicense or
distribute the Program is void, and will automatically terminate your rights under this
License. However, parties who have received copies, or rights, from you under this
License will not have their licenses terminated so long as such parties remain in full
compliance.
5. You are not required to accept this License, since you have not signed it. However,
nothing else grants you permission to modify or distribute the Program or its derivative
works. These actions are prohibited by law if you do not accept this License. Therefore,
by modifying or distributing the Program (or any work based on the Program), you
indicate your acceptance of this License to do so, and all its terms and conditions for
copying, distributing or modifying the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program), the
recipient automatically receives a license from the original licensor to copy, distribute or
modify the Program subject to these terms and conditions. You may not impose any
further restrictions on the recipients' exercise of the rights granted herein. You are not
responsible for enforcing compliance by third parties to this License.
245
7. If, as a consequence of a court judgment or allegation of patent infringement or for
any other reason (not limited to patent issues), conditions are imposed on you (whether
by court order, agreement or otherwise) that contradict the conditions of this License,
they do not excuse you from the conditions of this License. If you cannot distribute so
as to satisfy simultaneously your obligations under this License and any other pertinent
obligations, then as a consequence you may not distribute the Program at all. For
example, if a patent license would not permit royalty-free redistribution of the Program
by all those who receive copies directly or indirectly through you, then the only way
you could satisfy both it and this License would be to refrain entirely from distribution
of the Program.
If any portion of this section is held invalid or unenforceable under any particular
circumstance, the balance of the section is intended to apply and the section as a whole
is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other
property right claims or to contest validity of any such claims; this section has the sole
purpose of protecting the integrity of the free software distribution system, which is
implemented by public license practices. Many people have made generous
contributions to the wide range of software distributed through that system in reliance
on consistent application of that system; it is up to the author/donor to decide if he or
she is willing to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence
of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain countries either by
patents or by copyrighted interfaces, the original copyright holder who places the
Program under this License may add an explicit geographical distribution limitation
excluding those countries, so that distribution is permitted only in or among countries
not thus excluded. In such case, this License incorporates the limitation as if written in
the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of the
General Public License from time to time. Such new versions will be similar in spirit to
the present version, but may differ in detail to address new problems or concerns.
246
Thanks for reading!
Visit http://flossmanuals.net to make corrections or to find more manuals.